blob: d9fbeac682f8bbd8dd64b31caef0f3a2a117052e [file] [log] [blame]
/*
* 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.android.contacts.common.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.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactPresenceIconUtil;
import com.android.contacts.common.ContactStatusUtil;
import com.android.contacts.common.ContactTileLoaderFactory;
import com.android.contacts.common.MoreContactUtils;
import com.android.contacts.common.R;
import java.util.ArrayList;
/**
* Arranges contacts 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;
protected Cursor mContactCursor = null;
private ContactPhotoManager mPhotoManager;
protected int mNumFrequents;
/**
* Index of the first NON starred contact in the {@link Cursor}
* Only valid when {@link DisplayType#STREQUENT} is true
*/
private int mDividerPosition;
protected int mColumnCount;
private int mStarredIndex;
protected int mIdIndex;
protected int mLookupIndex;
protected int mPhotoUriIndex;
protected int mNameIndex;
protected int mPresenceIndex;
protected int mStatusIndex;
/**
* 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 com.android.contacts.GroupMemberLoader}
* when passing {@link Cursor} into loadFromCusor method.
*
* Group member logic has been moved into GroupMemberTileAdapter. This constant is still
* needed by calling classes.
*/
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}.
*/
protected void bindColumnIndices() {
mIdIndex = ContactTileLoaderFactory.CONTACT_ID;
mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY;
mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI;
mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME;
mStarredIndex = ContactTileLoaderFactory.STARRED;
mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE;
mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS;
mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER;
mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE;
mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL;
}
/**
* Gets the number of frequents from the passed in cursor.
*
* This methods is needed so the GroupMemberTileAdapter can override this.
*
* @param cursor The cursor to get number of frequents from.
*/
protected void saveNumFrequentsFromCursor(Cursor cursor) {
// count the number of frequents
switch (mDisplayType) {
case STARRED_ONLY:
mNumFrequents = 0;
break;
case STREQUENT:
case STREQUENT_PHONE_ONLY:
mNumFrequents = cursor.getCount() - mDividerPosition;
break;
case FREQUENT_ONLY:
mNumFrequents = cursor.getCount();
break;
default:
throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType);
}
}
/**
* Creates {@link ContactTileView}s for each item in {@link Cursor}.
*
* Else use {@link ContactTileLoaderFactory}
*/
public void setContactCursor(Cursor cursor) {
mContactCursor = cursor;
mDividerPosition = getDividerPosition(cursor);
saveNumFrequentsFromCursor(cursor);
// 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}
* Returns 0 if {@link DisplayType#FREQUENT_ONLY}
*/
protected 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 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();
}
protected 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);
String lookupKey = cursor.getString(mLookupIndex);
ContactEntry contact = new ContactEntry();
String name = cursor.getString(mNameIndex);
contact.name = (name != null) ? name : mResources.getString(R.string.missing_name);
contact.status = cursor.getString(mStatusIndex);
contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
contact.lookupKey = lookupKey;
contact.lookupUri = ContentUris.withAppendedId(
Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
contact.isFavorite = cursor.getInt(mStarredIndex) > 0;
// Set phone number and label
if (mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) {
int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex);
String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex);
contact.phoneLabel = (String) Phone.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:
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.
*/
protected 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:
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 MoreContactUtils.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:
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.
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.setPaddingRelative(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());
}
}
protected 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;
}
}