/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.silentcircle.contacts.list; import android.content.ContentUris; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.FrameLayout; import com.silentcircle.contacts.ContactPhotoManager; // import com.android.contacts.ContactPresenceIconUtil; //import com.silentcircle.silentcontacts.ContactStatusUtil; import com.silentcircle.contacts.ContactTileLoaderFactory; import com.silentcircle.contacts.ContactsUtils; import com.silentcircle.contacts.GroupMemberLoader; import com.silentcircle.contacts.GroupMemberLoader.GroupDetailQuery; import com.silentcircle.contacts.R; import com.silentcircle.silentcontacts.ScContactsContract.RawContacts; import com.silentcircle.contacts.model.account.LabelHelper.PhoneLabel; import java.util.ArrayList; /** * Arranges contacts in {@link ContactTileListFragment} (aka favorites) according to * provided {@link DisplayType}. * Also allows for a configurable number of columns and {@link DisplayType} */ public class ContactTileAdapter extends BaseAdapter { private static final String TAG = ContactTileAdapter.class.getSimpleName(); private DisplayType mDisplayType; private ContactTileView.Listener mListener; private Context mContext; private Resources mResources; private Cursor mContactCursor = null; private ContactPhotoManager mPhotoManager; private int mNumFrequents; /** * Index of the first NON starred contact in the {@link Cursor} * Only valid when {@link DisplayType#STREQUENT} is true */ private int mDividerPosition; private int mColumnCount; private int mIdIndex; private int mPhotoUriIndex; private int mNameIndex; private int mStarredIndex; /** * Only valid when {@link DisplayType#STREQUENT_PHONE_ONLY} is true */ private int mPhoneNumberIndex; private int mPhoneNumberTypeIndex; private int mPhoneNumberLabelIndex; private boolean mIsQuickContactEnabled = false; private final int mPaddingInPixels; /** * Configures the adapter to filter and display contacts using different view types. * TODO: Create Uris to support getting Starred_only and Frequent_only cursors. */ public enum DisplayType { /** * Displays a mixed view type of starred and frequent contacts */ STREQUENT, /** * Displays a mixed view type of starred and frequent contacts based on phone data. * Also includes secondary touch target. */ STREQUENT_PHONE_ONLY, /** * Display only starred contacts */ STARRED_ONLY, /** * Display only most frequently contacted */ FREQUENT_ONLY, /** * Display all contacts from a group in the cursor * Use {@link GroupMemberLoader} * when passing {@link Cursor} into loadFromCusor method. */ GROUP_MEMBERS } public ContactTileAdapter(Context context, ContactTileView.Listener listener, int numCols, DisplayType displayType) { mListener = listener; mContext = context; mResources = context.getResources(); mColumnCount = (displayType == DisplayType.FREQUENT_ONLY ? 1 : numCols); mDisplayType = displayType; mNumFrequents = 0; // Converting padding in dips to padding in pixels mPaddingInPixels = mContext.getResources().getDimensionPixelSize(R.dimen.contact_tile_divider_padding); bindColumnIndices(); } public void setPhotoLoader(ContactPhotoManager photoLoader) { mPhotoManager = photoLoader; } public void setColumnCount(int columnCount) { mColumnCount = columnCount; } public void setDisplayType(DisplayType displayType) { mDisplayType = displayType; } public void enableQuickContact(boolean enableQuickContact) { mIsQuickContactEnabled = enableQuickContact; } /** * Sets the column indices for expected {@link Cursor} * based on {@link DisplayType}. */ private void bindColumnIndices() { /** * Need to check for {@link DisplayType#GROUP_MEMBERS} because * it has different projections than all other {@link DisplayType}s * By using {@link GroupMemberLoader} and {@link ContactTileLoaderFactory} * the correct {@link Cursor}s will be given. */ if (mDisplayType == DisplayType.GROUP_MEMBERS) { mIdIndex = GroupDetailQuery.CONTACT_ID; mPhotoUriIndex = GroupDetailQuery.CONTACT_PHOTO_URI; mNameIndex = GroupDetailQuery.CONTACT_DISPLAY_NAME_PRIMARY; } else { mIdIndex = ContactTileLoaderFactory.CONTACT_ID; mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI; mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME; mStarredIndex = ContactTileLoaderFactory.STARRED; mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER; mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE; mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL; } } /** * Creates {@link ContactTileView}s for each item in {@link Cursor}. * If {@link DisplayType} is {@link DisplayType#GROUP_MEMBERS} use {@link GroupMemberLoader} * Else use {@link ContactTileLoaderFactory} */ public void setContactCursor(Cursor cursor) { mContactCursor = cursor; mDividerPosition = getDividerPosition(cursor); // count the number of frequents switch (mDisplayType) { case STARRED_ONLY: case GROUP_MEMBERS: mNumFrequents = 0; break; case STREQUENT: case STREQUENT_PHONE_ONLY: mNumFrequents = mContactCursor.getCount() - mDividerPosition; break; case FREQUENT_ONLY: mNumFrequents = mContactCursor.getCount(); break; default: throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType); } // cause a refresh of any views that rely on this data notifyDataSetChanged(); } /** * Iterates over the {@link Cursor} * Returns position of the first NON Starred Contact * Returns -1 if {@link DisplayType#STARRED_ONLY} or {@link DisplayType#GROUP_MEMBERS} * Returns 0 if {@link DisplayType#FREQUENT_ONLY} */ private int getDividerPosition(Cursor cursor) { if (cursor == null || cursor.isClosed()) { throw new IllegalStateException("Unable to access cursor"); } switch (mDisplayType) { case STREQUENT: case STREQUENT_PHONE_ONLY: cursor.moveToPosition(-1); while (cursor.moveToNext()) { if (cursor.getInt(mStarredIndex) == 0) { return cursor.getPosition(); } } break; case GROUP_MEMBERS: case STARRED_ONLY: // There is no divider return -1; case FREQUENT_ONLY: // Divider is first return 0; default: throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); } // There are not NON Starred contacts in cursor // Set divider positon to end return cursor.getCount(); } private ContactEntry createContactEntryFromCursor(Cursor cursor, int position) { // If the loader was canceled we will be given a null cursor. // In that case, show an empty list of contacts. if (cursor == null || cursor.isClosed() || cursor.getCount() <= position) return null; cursor.moveToPosition(position); long id = cursor.getLong(mIdIndex); String photoUri = cursor.getString(mPhotoUriIndex); ContactEntry contact = new ContactEntry(); String name = cursor.getString(mNameIndex); contact.name = (name != null) ? name : mResources.getString(R.string.missing_name); contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); contact.lookupKey = ContentUris.withAppendedId(RawContacts.CONTENT_URI, id); // Set phone number and label if (mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) { int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); contact.phoneLabel = (String) PhoneLabel.getTypeLabel(mResources, phoneNumberType, phoneNumberCustomLabel); contact.phoneNumber = cursor.getString(mPhoneNumberIndex); } else { // Set presence icon and status message Drawable icon = null; int presence = 0; // if (!cursor.isNull(mPresenceIndex)) { // presence = cursor.getInt(mPresenceIndex); // icon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence); // } // contact.presenceIcon = icon; String statusMessage = null; // if (mStatusIndex != 0 && !cursor.isNull(mStatusIndex)) { // statusMessage = cursor.getString(mStatusIndex); // } // If there is no status message from the contact, but there was a presence value, // then use the default status message string // if (statusMessage == null && presence != 0) { // statusMessage = ContactStatusUtil.getStatusString(mContext, presence); // } contact.status = statusMessage; } return contact; } /** * Returns the number of frequents that will be displayed in the list. */ public int getNumFrequents() { return mNumFrequents; } @Override public int getCount() { if (mContactCursor == null || mContactCursor.isClosed()) { return 0; } switch (mDisplayType) { case STARRED_ONLY: case GROUP_MEMBERS: return getRowCount(mContactCursor.getCount()); case STREQUENT: case STREQUENT_PHONE_ONLY: // Takes numbers of rows the Starred Contacts Occupy int starredRowCount = getRowCount(mDividerPosition); // Compute the frequent row count which is 1 plus the number of frequents // (to account for the divider) or 0 if there are no frequents. int frequentRowCount = mNumFrequents == 0 ? 0 : mNumFrequents + 1; // Return the number of starred plus frequent rows return starredRowCount + frequentRowCount; case FREQUENT_ONLY: // Number of frequent contacts return mContactCursor.getCount(); default: throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType); } } /** * Returns the number of rows required to show the provided number of entries * with the current number of columns. */ private int getRowCount(int entryCount) { return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1; } public int getColumnCount() { return mColumnCount; } /** * Returns an ArrayList of the {@link ContactEntry}s that are to appear * on the row for the given position. */ @Override public ArrayList<ContactEntry> getItem(int position) { ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount); int contactIndex = position * mColumnCount; switch (mDisplayType) { case FREQUENT_ONLY: resultList.add(createContactEntryFromCursor(mContactCursor, position)); break; case STARRED_ONLY: case GROUP_MEMBERS: for (int columnCounter = 0; columnCounter < mColumnCount; columnCounter++) { resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); contactIndex++; } break; case STREQUENT: case STREQUENT_PHONE_ONLY: if (position < getRowCount(mDividerPosition)) { for (int columnCounter = 0; columnCounter < mColumnCount && contactIndex != mDividerPosition; columnCounter++) { resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); contactIndex++; } } else { /* * Current position minus how many rows are before the divider and * Minus 1 for the divider itself provides the relative index of the frequent * contact being displayed. Then add the dividerPostion to give the offset * into the contacts cursor to get the absoulte index. */ contactIndex = position - getRowCount(mDividerPosition) - 1 + mDividerPosition; resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); } break; default: throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); } return resultList; } @Override public long getItemId(int position) { // As we show several selectable items for each ListView row, // we can not determine a stable id. But as we don't rely on ListView's selection, // this should not be a problem. return position; } @Override public boolean areAllItemsEnabled() { return (mDisplayType != DisplayType.STREQUENT && mDisplayType != DisplayType.STREQUENT_PHONE_ONLY); } @Override public boolean isEnabled(int position) { return position != getRowCount(mDividerPosition); } @Override public View getView(int position, View convertView, ViewGroup parent) { int itemViewType = getItemViewType(position); if (itemViewType == ViewTypes.DIVIDER) { // Checking For Divider First so not to cast convertView return convertView == null ? getDivider() : convertView; } ContactTileRow contactTileRowView = (ContactTileRow) convertView; ArrayList<ContactEntry> contactList = getItem(position); if (contactTileRowView == null) { // Creating new row if needed contactTileRowView = new ContactTileRow(mContext, itemViewType); } contactTileRowView.configureRow(contactList, position == getCount() - 1); return contactTileRowView; } /** * Divider uses a list_seperator.xml along with text to denote * the most frequently contacted contacts. */ public View getDivider() { return ContactsUtils.createHeaderView(mContext, mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? R.string.favoritesFrequentCalled : R.string.favoritesFrequentContacted); } private int getLayoutResourceId(int viewType) { switch (viewType) { case ViewTypes.STARRED: return mIsQuickContactEnabled ? R.layout.contact_tile_starred_quick_contact : R.layout.contact_tile_starred; case ViewTypes.FREQUENT: return mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? R.layout.contact_tile_frequent_phone : R.layout.contact_tile_frequent; case ViewTypes.STARRED_PHONE: return R.layout.contact_tile_phone_starred; default: throw new IllegalArgumentException("Unrecognized viewType " + viewType); } } @Override public int getViewTypeCount() { return ViewTypes.COUNT; } @Override public int getItemViewType(int position) { /* * Returns view type based on {@link DisplayType}. * {@link DisplayType#STARRED_ONLY} and {@link DisplayType#GROUP_MEMBERS} * are {@link ViewTypes#STARRED}. * {@link DisplayType#FREQUENT_ONLY} is {@link ViewTypes#FREQUENT}. * {@link DisplayType#STREQUENT} mixes both {@link ViewTypes} * and also adds in {@link ViewTypes#DIVIDER}. */ switch (mDisplayType) { case STREQUENT: if (position < getRowCount(mDividerPosition)) { return ViewTypes.STARRED; } else if (position == getRowCount(mDividerPosition)) { return ViewTypes.DIVIDER; } else { return ViewTypes.FREQUENT; } case STREQUENT_PHONE_ONLY: if (position < getRowCount(mDividerPosition)) { return ViewTypes.STARRED_PHONE; } else if (position == getRowCount(mDividerPosition)) { return ViewTypes.DIVIDER; } else { return ViewTypes.FREQUENT; } case STARRED_ONLY: case GROUP_MEMBERS: return ViewTypes.STARRED; case FREQUENT_ONLY: return ViewTypes.FREQUENT; default: throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); } } /** * Returns the "frequent header" position. Only available when STREQUENT or * STREQUENT_PHONE_ONLY is used for its display type. */ public int getFrequentHeaderPosition() { return getRowCount(mDividerPosition); } /** * Acts as a row item composed of {@link ContactTileView} * * TODO: FREQUENT doesn't really need it. Just let {@link #getView} return */ private class ContactTileRow extends FrameLayout { private int mItemViewType; private int mLayoutResId; public ContactTileRow(Context context, int itemViewType) { super(context); mItemViewType = itemViewType; mLayoutResId = getLayoutResourceId(mItemViewType); // Remove row (but not children) from accessibility node tree. // TODO setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); } /** * Configures the row to add {@link ContactEntry}s information to the views */ public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) { int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount; // Adding tiles to row and filling in contact information for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) { ContactEntry entry = columnCounter < list.size() ? list.get(columnCounter) : null; addTileFromEntry(entry, columnCounter, isLastRow); } } private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) { final ContactTileView contactTile; if (getChildCount() <= childIndex) { contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null); // Note: the layoutparam set here is only actually used for FREQUENT. // We override onMeasure() for STARRED and we don't care the layout param there. Resources resources = mContext.getResources(); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.setMargins(resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0, resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 0); contactTile.setLayoutParams(params); contactTile.setPhotoManager(mPhotoManager); contactTile.setListener(mListener); addView(contactTile); } else { contactTile = (ContactTileView) getChildAt(childIndex); } contactTile.loadFromContact(entry); switch (mItemViewType) { case ViewTypes.STARRED_PHONE: case ViewTypes.STARRED: // Setting divider visibilities contactTile.setPadding(0, 0, childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, isLastRow ? 0 : mPaddingInPixels); break; case ViewTypes.FREQUENT: contactTile.setHorizontalDividerVisibility(isLastRow ? View.GONE : View.VISIBLE); break; default: break; } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { switch (mItemViewType) { case ViewTypes.STARRED_PHONE: case ViewTypes.STARRED: onLayoutForTiles(); return; default: super.onLayout(changed, left, top, right, bottom); return; } } private void onLayoutForTiles() { final int count = getChildCount(); // Just line up children horizontally. int childLeft = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); // Note MeasuredWidth includes the padding. final int childWidth = child.getMeasuredWidth(); child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); childLeft += childWidth; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { switch (mItemViewType) { case ViewTypes.STARRED_PHONE: case ViewTypes.STARRED: onMeasureForTiles(widthMeasureSpec); return; default: super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } } private void onMeasureForTiles(int widthMeasureSpec) { final int width = MeasureSpec.getSize(widthMeasureSpec); final int childCount = getChildCount(); if (childCount == 0) { // Just in case... setMeasuredDimension(width, 0); return; } // 1. Calculate image size. // = ([total width] - [total padding]) / [child count] // // 2. Set it to width/height of each children. // If we have a remainder, some tiles will have 1 pixel larger width than its height. // // 3. Set the dimensions of itself. // Let width = given width. // Let height = image size + bottom paddding. final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels; // Preferred width / height for images (excluding the padding). // The actual width may be 1 pixel larger than this if we have a remainder. final int imageSize = (width - totalPaddingsInPixels) / mColumnCount; final int remainder = width - (imageSize * mColumnCount) - totalPaddingsInPixels; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final int childWidth = imageSize + child.getPaddingRight() // Compensate for the remainder + (i < remainder ? 1 : 0); final int childHeight = imageSize + child.getPaddingBottom(); child.measure( MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY) ); } setMeasuredDimension(width, imageSize + getChildAt(0).getPaddingBottom()); } } /** * Class to hold contact information */ public static class ContactEntry { public String name; public String status; public String phoneLabel; public String phoneNumber; public Uri photoUri; public Uri lookupKey; // public Drawable presenceIcon; } private static class ViewTypes { public static final int COUNT = 4; public static final int STARRED = 0; public static final int DIVIDER = 1; public static final int FREQUENT = 2; public static final int STARRED_PHONE = 3; } }