Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts.list;
     17 
     18 import android.content.ContentUris;
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.database.Cursor;
     22 import android.graphics.drawable.Drawable;
     23 import android.net.Uri;
     24 import android.provider.ContactsContract.CommonDataKinds.Phone;
     25 import android.provider.ContactsContract.Contacts;
     26 import android.view.View;
     27 import android.view.ViewGroup;
     28 import android.widget.BaseAdapter;
     29 import android.widget.FrameLayout;
     30 
     31 import com.android.contacts.ContactPhotoManager;
     32 import com.android.contacts.ContactPresenceIconUtil;
     33 import com.android.contacts.ContactStatusUtil;
     34 import com.android.contacts.ContactTileLoaderFactory;
     35 import com.android.contacts.ContactsUtils;
     36 import com.android.contacts.GroupMemberLoader;
     37 import com.android.contacts.GroupMemberLoader.GroupDetailQuery;
     38 import com.android.contacts.R;
     39 
     40 import java.util.ArrayList;
     41 
     42 /**
     43  * Arranges contacts in {@link ContactTileListFragment} (aka favorites) according to
     44  * provided {@link DisplayType}.
     45  * Also allows for a configurable number of columns and {@link DisplayType}
     46  */
     47 public class ContactTileAdapter extends BaseAdapter {
     48     private static final String TAG = ContactTileAdapter.class.getSimpleName();
     49 
     50     private DisplayType mDisplayType;
     51     private ContactTileView.Listener mListener;
     52     private Context mContext;
     53     private Resources mResources;
     54     private Cursor mContactCursor = null;
     55     private ContactPhotoManager mPhotoManager;
     56     private int mNumFrequents;
     57 
     58     /**
     59      * Index of the first NON starred contact in the {@link Cursor}
     60      * Only valid when {@link DisplayType#STREQUENT} is true
     61      */
     62     private int mDividerPosition;
     63     private int mColumnCount;
     64     private int mIdIndex;
     65     private int mLookupIndex;
     66     private int mPhotoUriIndex;
     67     private int mNameIndex;
     68     private int mStarredIndex;
     69     private int mPresenceIndex;
     70     private int mStatusIndex;
     71 
     72     /**
     73      * Only valid when {@link DisplayType#STREQUENT_PHONE_ONLY} is true
     74      */
     75     private int mPhoneNumberIndex;
     76     private int mPhoneNumberTypeIndex;
     77     private int mPhoneNumberLabelIndex;
     78 
     79     private boolean mIsQuickContactEnabled = false;
     80     private final int mPaddingInPixels;
     81 
     82     /**
     83      * Configures the adapter to filter and display contacts using different view types.
     84      * TODO: Create Uris to support getting Starred_only and Frequent_only cursors.
     85      */
     86     public enum DisplayType {
     87         /**
     88          * Displays a mixed view type of starred and frequent contacts
     89          */
     90         STREQUENT,
     91 
     92         /**
     93          * Displays a mixed view type of starred and frequent contacts based on phone data.
     94          * Also includes secondary touch target.
     95          */
     96         STREQUENT_PHONE_ONLY,
     97 
     98         /**
     99          * Display only starred contacts
    100          */
    101         STARRED_ONLY,
    102 
    103         /**
    104          * Display only most frequently contacted
    105          */
    106         FREQUENT_ONLY,
    107 
    108         /**
    109          * Display all contacts from a group in the cursor
    110          * Use {@link GroupMemberLoader}
    111          * when passing {@link Cursor} into loadFromCusor method.
    112          */
    113         GROUP_MEMBERS
    114     }
    115 
    116     public ContactTileAdapter(Context context, ContactTileView.Listener listener, int numCols,
    117             DisplayType displayType) {
    118         mListener = listener;
    119         mContext = context;
    120         mResources = context.getResources();
    121         mColumnCount = (displayType == DisplayType.FREQUENT_ONLY ? 1 : numCols);
    122         mDisplayType = displayType;
    123         mNumFrequents = 0;
    124 
    125         // Converting padding in dips to padding in pixels
    126         mPaddingInPixels = mContext.getResources()
    127                 .getDimensionPixelSize(R.dimen.contact_tile_divider_padding);
    128 
    129         bindColumnIndices();
    130     }
    131 
    132     public void setPhotoLoader(ContactPhotoManager photoLoader) {
    133         mPhotoManager = photoLoader;
    134     }
    135 
    136     public void setColumnCount(int columnCount) {
    137         mColumnCount = columnCount;
    138     }
    139 
    140     public void setDisplayType(DisplayType displayType) {
    141         mDisplayType = displayType;
    142     }
    143 
    144     public void enableQuickContact(boolean enableQuickContact) {
    145         mIsQuickContactEnabled = enableQuickContact;
    146     }
    147 
    148     /**
    149      * Sets the column indices for expected {@link Cursor}
    150      * based on {@link DisplayType}.
    151      */
    152     private void bindColumnIndices() {
    153         /**
    154          * Need to check for {@link DisplayType#GROUP_MEMBERS} because
    155          * it has different projections than all other {@link DisplayType}s
    156          * By using {@link GroupMemberLoader} and {@link ContactTileLoaderFactory}
    157          * the correct {@link Cursor}s will be given.
    158          */
    159         if (mDisplayType == DisplayType.GROUP_MEMBERS) {
    160             mIdIndex = GroupDetailQuery.CONTACT_ID;
    161             mLookupIndex = GroupDetailQuery.CONTACT_LOOKUP_KEY;
    162             mPhotoUriIndex = GroupDetailQuery.CONTACT_PHOTO_URI;
    163             mNameIndex = GroupDetailQuery.CONTACT_DISPLAY_NAME_PRIMARY;
    164             mPresenceIndex = GroupDetailQuery.CONTACT_PRESENCE_STATUS;
    165             mStatusIndex = GroupDetailQuery.CONTACT_STATUS;
    166         } else {
    167             mIdIndex = ContactTileLoaderFactory.CONTACT_ID;
    168             mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY;
    169             mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI;
    170             mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME;
    171             mStarredIndex = ContactTileLoaderFactory.STARRED;
    172             mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE;
    173             mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS;
    174 
    175             mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER;
    176             mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE;
    177             mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL;
    178         }
    179     }
    180 
    181     /**
    182      * Creates {@link ContactTileView}s for each item in {@link Cursor}.
    183      * If {@link DisplayType} is {@link DisplayType#GROUP_MEMBERS} use {@link GroupMemberLoader}
    184      * Else use {@link ContactTileLoaderFactory}
    185      */
    186     public void setContactCursor(Cursor cursor) {
    187         mContactCursor = cursor;
    188         mDividerPosition = getDividerPosition(cursor);
    189 
    190         // count the number of frequents
    191         switch (mDisplayType) {
    192             case STARRED_ONLY:
    193             case GROUP_MEMBERS:
    194                 mNumFrequents = 0;
    195                 break;
    196             case STREQUENT:
    197             case STREQUENT_PHONE_ONLY:
    198                 mNumFrequents = mContactCursor.getCount() - mDividerPosition;
    199                 break;
    200             case FREQUENT_ONLY:
    201                 mNumFrequents = mContactCursor.getCount();
    202                 break;
    203             default:
    204                 throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType);
    205         }
    206 
    207         // cause a refresh of any views that rely on this data
    208         notifyDataSetChanged();
    209     }
    210 
    211     /**
    212      * Iterates over the {@link Cursor}
    213      * Returns position of the first NON Starred Contact
    214      * Returns -1 if {@link DisplayType#STARRED_ONLY} or {@link DisplayType#GROUP_MEMBERS}
    215      * Returns 0 if {@link DisplayType#FREQUENT_ONLY}
    216      */
    217     private int getDividerPosition(Cursor cursor) {
    218         if (cursor == null || cursor.isClosed()) {
    219             throw new IllegalStateException("Unable to access cursor");
    220         }
    221 
    222         switch (mDisplayType) {
    223             case STREQUENT:
    224             case STREQUENT_PHONE_ONLY:
    225                 cursor.moveToPosition(-1);
    226                 while (cursor.moveToNext()) {
    227                     if (cursor.getInt(mStarredIndex) == 0) {
    228                         return cursor.getPosition();
    229                     }
    230                 }
    231                 break;
    232             case GROUP_MEMBERS:
    233             case STARRED_ONLY:
    234                 // There is no divider
    235                 return -1;
    236             case FREQUENT_ONLY:
    237                 // Divider is first
    238                 return 0;
    239             default:
    240                 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType);
    241         }
    242 
    243         // There are not NON Starred contacts in cursor
    244         // Set divider positon to end
    245         return cursor.getCount();
    246     }
    247 
    248     private ContactEntry createContactEntryFromCursor(Cursor cursor, int position) {
    249         // If the loader was canceled we will be given a null cursor.
    250         // In that case, show an empty list of contacts.
    251         if (cursor == null || cursor.isClosed() || cursor.getCount() <= position) return null;
    252 
    253         cursor.moveToPosition(position);
    254         long id = cursor.getLong(mIdIndex);
    255         String photoUri = cursor.getString(mPhotoUriIndex);
    256         String lookupKey = cursor.getString(mLookupIndex);
    257 
    258         ContactEntry contact = new ContactEntry();
    259         String name = cursor.getString(mNameIndex);
    260         contact.name = (name != null) ? name : mResources.getString(R.string.missing_name);
    261         contact.status = cursor.getString(mStatusIndex);
    262         contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
    263         contact.lookupKey = ContentUris.withAppendedId(
    264                 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
    265 
    266         // Set phone number and label
    267         if (mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) {
    268             int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex);
    269             String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex);
    270             contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType,
    271                     phoneNumberCustomLabel);
    272             contact.phoneNumber = cursor.getString(mPhoneNumberIndex);
    273         } else {
    274             // Set presence icon and status message
    275             Drawable icon = null;
    276             int presence = 0;
    277             if (!cursor.isNull(mPresenceIndex)) {
    278                 presence = cursor.getInt(mPresenceIndex);
    279                 icon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence);
    280             }
    281             contact.presenceIcon = icon;
    282 
    283             String statusMessage = null;
    284             if (mStatusIndex != 0 && !cursor.isNull(mStatusIndex)) {
    285                 statusMessage = cursor.getString(mStatusIndex);
    286             }
    287             // If there is no status message from the contact, but there was a presence value,
    288             // then use the default status message string
    289             if (statusMessage == null && presence != 0) {
    290                 statusMessage = ContactStatusUtil.getStatusString(mContext, presence);
    291             }
    292             contact.status = statusMessage;
    293         }
    294 
    295         return contact;
    296     }
    297 
    298     /**
    299      * Returns the number of frequents that will be displayed in the list.
    300      */
    301     public int getNumFrequents() {
    302         return mNumFrequents;
    303     }
    304 
    305     @Override
    306     public int getCount() {
    307         if (mContactCursor == null || mContactCursor.isClosed()) {
    308             return 0;
    309         }
    310 
    311         switch (mDisplayType) {
    312             case STARRED_ONLY:
    313             case GROUP_MEMBERS:
    314                 return getRowCount(mContactCursor.getCount());
    315             case STREQUENT:
    316             case STREQUENT_PHONE_ONLY:
    317                 // Takes numbers of rows the Starred Contacts Occupy
    318                 int starredRowCount = getRowCount(mDividerPosition);
    319 
    320                 // Compute the frequent row count which is 1 plus the number of frequents
    321                 // (to account for the divider) or 0 if there are no frequents.
    322                 int frequentRowCount = mNumFrequents == 0 ? 0 : mNumFrequents + 1;
    323 
    324                 // Return the number of starred plus frequent rows
    325                 return starredRowCount + frequentRowCount;
    326             case FREQUENT_ONLY:
    327                 // Number of frequent contacts
    328                 return mContactCursor.getCount();
    329             default:
    330                 throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType);
    331         }
    332     }
    333 
    334     /**
    335      * Returns the number of rows required to show the provided number of entries
    336      * with the current number of columns.
    337      */
    338     private int getRowCount(int entryCount) {
    339         return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1;
    340     }
    341 
    342     public int getColumnCount() {
    343         return mColumnCount;
    344     }
    345 
    346     /**
    347      * Returns an ArrayList of the {@link ContactEntry}s that are to appear
    348      * on the row for the given position.
    349      */
    350     @Override
    351     public ArrayList<ContactEntry> getItem(int position) {
    352         ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount);
    353         int contactIndex = position * mColumnCount;
    354 
    355         switch (mDisplayType) {
    356             case FREQUENT_ONLY:
    357                 resultList.add(createContactEntryFromCursor(mContactCursor, position));
    358                 break;
    359             case STARRED_ONLY:
    360             case GROUP_MEMBERS:
    361                 for (int columnCounter = 0; columnCounter < mColumnCount; columnCounter++) {
    362                     resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
    363                     contactIndex++;
    364                 }
    365                 break;
    366             case STREQUENT:
    367             case STREQUENT_PHONE_ONLY:
    368                 if (position < getRowCount(mDividerPosition)) {
    369                     for (int columnCounter = 0; columnCounter < mColumnCount &&
    370                             contactIndex != mDividerPosition; columnCounter++) {
    371                         resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
    372                         contactIndex++;
    373                     }
    374                 } else {
    375                     /*
    376                      * Current position minus how many rows are before the divider and
    377                      * Minus 1 for the divider itself provides the relative index of the frequent
    378                      * contact being displayed. Then add the dividerPostion to give the offset
    379                      * into the contacts cursor to get the absoulte index.
    380                      */
    381                     contactIndex = position - getRowCount(mDividerPosition) - 1 + mDividerPosition;
    382                     resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
    383                 }
    384                 break;
    385             default:
    386                 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType);
    387         }
    388         return resultList;
    389     }
    390 
    391     @Override
    392     public long getItemId(int position) {
    393         // As we show several selectable items for each ListView row,
    394         // we can not determine a stable id. But as we don't rely on ListView's selection,
    395         // this should not be a problem.
    396         return position;
    397     }
    398 
    399     @Override
    400     public boolean areAllItemsEnabled() {
    401         return (mDisplayType != DisplayType.STREQUENT &&
    402                 mDisplayType != DisplayType.STREQUENT_PHONE_ONLY);
    403     }
    404 
    405     @Override
    406     public boolean isEnabled(int position) {
    407         return position != getRowCount(mDividerPosition);
    408     }
    409 
    410     @Override
    411     public View getView(int position, View convertView, ViewGroup parent) {
    412         int itemViewType = getItemViewType(position);
    413 
    414         if (itemViewType == ViewTypes.DIVIDER) {
    415             // Checking For Divider First so not to cast convertView
    416             return convertView == null ? getDivider() : convertView;
    417         }
    418 
    419         ContactTileRow contactTileRowView = (ContactTileRow) convertView;
    420         ArrayList<ContactEntry> contactList = getItem(position);
    421 
    422         if (contactTileRowView == null) {
    423             // Creating new row if needed
    424             contactTileRowView = new ContactTileRow(mContext, itemViewType);
    425         }
    426 
    427         contactTileRowView.configureRow(contactList, position == getCount() - 1);
    428         return contactTileRowView;
    429     }
    430 
    431     /**
    432      * Divider uses a list_seperator.xml along with text to denote
    433      * the most frequently contacted contacts.
    434      */
    435     public View getDivider() {
    436         return ContactsUtils.createHeaderView(mContext,
    437                 mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ?
    438                 R.string.favoritesFrequentCalled : R.string.favoritesFrequentContacted);
    439     }
    440 
    441     private int getLayoutResourceId(int viewType) {
    442         switch (viewType) {
    443             case ViewTypes.STARRED:
    444                 return mIsQuickContactEnabled ?
    445                         R.layout.contact_tile_starred_quick_contact : R.layout.contact_tile_starred;
    446             case ViewTypes.FREQUENT:
    447                 return mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ?
    448                         R.layout.contact_tile_frequent_phone : R.layout.contact_tile_frequent;
    449             case ViewTypes.STARRED_PHONE:
    450                 return R.layout.contact_tile_phone_starred;
    451             default:
    452                 throw new IllegalArgumentException("Unrecognized viewType " + viewType);
    453         }
    454     }
    455     @Override
    456     public int getViewTypeCount() {
    457         return ViewTypes.COUNT;
    458     }
    459 
    460     @Override
    461     public int getItemViewType(int position) {
    462         /*
    463          * Returns view type based on {@link DisplayType}.
    464          * {@link DisplayType#STARRED_ONLY} and {@link DisplayType#GROUP_MEMBERS}
    465          * are {@link ViewTypes#STARRED}.
    466          * {@link DisplayType#FREQUENT_ONLY} is {@link ViewTypes#FREQUENT}.
    467          * {@link DisplayType#STREQUENT} mixes both {@link ViewTypes}
    468          * and also adds in {@link ViewTypes#DIVIDER}.
    469          */
    470         switch (mDisplayType) {
    471             case STREQUENT:
    472                 if (position < getRowCount(mDividerPosition)) {
    473                     return ViewTypes.STARRED;
    474                 } else if (position == getRowCount(mDividerPosition)) {
    475                     return ViewTypes.DIVIDER;
    476                 } else {
    477                     return ViewTypes.FREQUENT;
    478                 }
    479             case STREQUENT_PHONE_ONLY:
    480                 if (position < getRowCount(mDividerPosition)) {
    481                     return ViewTypes.STARRED_PHONE;
    482                  } else if (position == getRowCount(mDividerPosition)) {
    483                     return ViewTypes.DIVIDER;
    484                 } else {
    485                     return ViewTypes.FREQUENT;
    486                 }
    487             case STARRED_ONLY:
    488             case GROUP_MEMBERS:
    489                 return ViewTypes.STARRED;
    490             case FREQUENT_ONLY:
    491                 return ViewTypes.FREQUENT;
    492             default:
    493                 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType);
    494         }
    495     }
    496 
    497     /**
    498      * Returns the "frequent header" position. Only available when STREQUENT or
    499      * STREQUENT_PHONE_ONLY is used for its display type.
    500      */
    501     public int getFrequentHeaderPosition() {
    502         return getRowCount(mDividerPosition);
    503     }
    504 
    505     /**
    506      * Acts as a row item composed of {@link ContactTileView}
    507      *
    508      * TODO: FREQUENT doesn't really need it.  Just let {@link #getView} return
    509      */
    510     private class ContactTileRow extends FrameLayout {
    511         private int mItemViewType;
    512         private int mLayoutResId;
    513 
    514         public ContactTileRow(Context context, int itemViewType) {
    515             super(context);
    516             mItemViewType = itemViewType;
    517             mLayoutResId = getLayoutResourceId(mItemViewType);
    518 
    519             // Remove row (but not children) from accessibility node tree.
    520             setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
    521         }
    522 
    523         /**
    524          * Configures the row to add {@link ContactEntry}s information to the views
    525          */
    526         public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) {
    527             int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount;
    528 
    529             // Adding tiles to row and filling in contact information
    530             for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) {
    531                 ContactEntry entry =
    532                         columnCounter < list.size() ? list.get(columnCounter) : null;
    533                 addTileFromEntry(entry, columnCounter, isLastRow);
    534             }
    535         }
    536 
    537         private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) {
    538             final ContactTileView contactTile;
    539 
    540             if (getChildCount() <= childIndex) {
    541                 contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null);
    542                 // Note: the layoutparam set here is only actually used for FREQUENT.
    543                 // We override onMeasure() for STARRED and we don't care the layout param there.
    544                 Resources resources = mContext.getResources();
    545                 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
    546                         ViewGroup.LayoutParams.WRAP_CONTENT,
    547                         ViewGroup.LayoutParams.WRAP_CONTENT);
    548                 params.setMargins(
    549                         resources.getDimensionPixelSize(R.dimen.detail_item_side_margin),
    550                         0,
    551                         resources.getDimensionPixelSize(R.dimen.detail_item_side_margin),
    552                         0);
    553                 contactTile.setLayoutParams(params);
    554                 contactTile.setPhotoManager(mPhotoManager);
    555                 contactTile.setListener(mListener);
    556                 addView(contactTile);
    557             } else {
    558                 contactTile = (ContactTileView) getChildAt(childIndex);
    559             }
    560             contactTile.loadFromContact(entry);
    561 
    562             switch (mItemViewType) {
    563                 case ViewTypes.STARRED_PHONE:
    564                 case ViewTypes.STARRED:
    565                     // Setting divider visibilities
    566                     contactTile.setPadding(0, 0,
    567                             childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels,
    568                             isLastRow ? 0 : mPaddingInPixels);
    569                     break;
    570                 case ViewTypes.FREQUENT:
    571                     contactTile.setHorizontalDividerVisibility(
    572                             isLastRow ? View.GONE : View.VISIBLE);
    573                     break;
    574                 default:
    575                     break;
    576             }
    577         }
    578 
    579         @Override
    580         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    581             switch (mItemViewType) {
    582                 case ViewTypes.STARRED_PHONE:
    583                 case ViewTypes.STARRED:
    584                     onLayoutForTiles();
    585                     return;
    586                 default:
    587                     super.onLayout(changed, left, top, right, bottom);
    588                     return;
    589             }
    590         }
    591 
    592         private void onLayoutForTiles() {
    593             final int count = getChildCount();
    594 
    595             // Just line up children horizontally.
    596             int childLeft = 0;
    597             for (int i = 0; i < count; i++) {
    598                 final View child = getChildAt(i);
    599 
    600                 // Note MeasuredWidth includes the padding.
    601                 final int childWidth = child.getMeasuredWidth();
    602                 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
    603                 childLeft += childWidth;
    604             }
    605         }
    606 
    607         @Override
    608         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    609             switch (mItemViewType) {
    610                 case ViewTypes.STARRED_PHONE:
    611                 case ViewTypes.STARRED:
    612                     onMeasureForTiles(widthMeasureSpec);
    613                     return;
    614                 default:
    615                     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    616                     return;
    617             }
    618         }
    619 
    620         private void onMeasureForTiles(int widthMeasureSpec) {
    621             final int width = MeasureSpec.getSize(widthMeasureSpec);
    622 
    623             final int childCount = getChildCount();
    624             if (childCount == 0) {
    625                 // Just in case...
    626                 setMeasuredDimension(width, 0);
    627                 return;
    628             }
    629 
    630             // 1. Calculate image size.
    631             //      = ([total width] - [total padding]) / [child count]
    632             //
    633             // 2. Set it to width/height of each children.
    634             //    If we have a remainder, some tiles will have 1 pixel larger width than its height.
    635             //
    636             // 3. Set the dimensions of itself.
    637             //    Let width = given width.
    638             //    Let height = image size + bottom paddding.
    639 
    640             final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels;
    641 
    642             // Preferred width / height for images (excluding the padding).
    643             // The actual width may be 1 pixel larger than this if we have a remainder.
    644             final int imageSize = (width - totalPaddingsInPixels) / mColumnCount;
    645             final int remainder = width - (imageSize * mColumnCount) - totalPaddingsInPixels;
    646 
    647             for (int i = 0; i < childCount; i++) {
    648                 final View child = getChildAt(i);
    649                 final int childWidth = imageSize + child.getPaddingRight()
    650                         // Compensate for the remainder
    651                         + (i < remainder ? 1 : 0);
    652                 final int childHeight = imageSize + child.getPaddingBottom();
    653                 child.measure(
    654                         MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
    655                         MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
    656                         );
    657             }
    658             setMeasuredDimension(width, imageSize + getChildAt(0).getPaddingBottom());
    659         }
    660     }
    661 
    662     /**
    663      * Class to hold contact information
    664      */
    665     public static class ContactEntry {
    666         public String name;
    667         public String status;
    668         public String phoneLabel;
    669         public String phoneNumber;
    670         public Uri photoUri;
    671         public Uri lookupKey;
    672         public Drawable presenceIcon;
    673     }
    674 
    675     private static class ViewTypes {
    676         public static final int COUNT = 4;
    677         public static final int STARRED = 0;
    678         public static final int DIVIDER = 1;
    679         public static final int FREQUENT = 2;
    680         public static final int STARRED_PHONE = 3;
    681     }
    682 }
    683