• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.contacts.ContactPhotoManager;
19 import com.android.contacts.ContactPresenceIconUtil;
20 import com.android.contacts.ContactStatusUtil;
21 import com.android.contacts.ContactTileLoaderFactory;
22 import com.android.contacts.ContactsUtils;
23 import com.android.contacts.GroupMemberLoader;
24 import com.android.contacts.GroupMemberLoader.GroupDetailQuery;
25 import com.android.contacts.R;
26 
27 import android.content.ContentUris;
28 import android.content.Context;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.graphics.drawable.Drawable;
32 import android.net.Uri;
33 import android.provider.ContactsContract.CommonDataKinds.Phone;
34 import android.provider.ContactsContract.Contacts;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.BaseAdapter;
38 import android.widget.FrameLayout;
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 
ContactTileAdapter(Context context, ContactTileView.Listener listener, int numCols, DisplayType displayType)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 
setPhotoLoader(ContactPhotoManager photoLoader)132     public void setPhotoLoader(ContactPhotoManager photoLoader) {
133         mPhotoManager = photoLoader;
134     }
135 
setColumnCount(int columnCount)136     public void setColumnCount(int columnCount) {
137         mColumnCount = columnCount;
138     }
139 
setDisplayType(DisplayType displayType)140     public void setDisplayType(DisplayType displayType) {
141         mDisplayType = displayType;
142     }
143 
enableQuickContact(boolean enableQuickContact)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      */
bindColumnIndices()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      */
setContactCursor(Cursor cursor)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      */
getDividerPosition(Cursor cursor)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 
createContactEntryFromCursor(Cursor cursor, int position)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      */
getNumFrequents()301     public int getNumFrequents() {
302         return mNumFrequents;
303     }
304 
305     @Override
getCount()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      */
getRowCount(int entryCount)338     private int getRowCount(int entryCount) {
339         return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1;
340     }
341 
getColumnCount()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
getItem(int position)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
getItemId(int position)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
areAllItemsEnabled()400     public boolean areAllItemsEnabled() {
401         return (mDisplayType != DisplayType.STREQUENT &&
402                 mDisplayType != DisplayType.STREQUENT_PHONE_ONLY);
403     }
404 
405     @Override
isEnabled(int position)406     public boolean isEnabled(int position) {
407         return position != getRowCount(mDividerPosition);
408     }
409 
410     @Override
getView(int position, View convertView, ViewGroup parent)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      */
getDivider()435     public View getDivider() {
436         return ContactsUtils.createHeaderView(mContext,
437                 mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ?
438                 R.string.favoritesFrequentCalled : R.string.favoritesFrequentContacted);
439     }
440 
getLayoutResourceId(int viewType)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
getViewTypeCount()456     public int getViewTypeCount() {
457         return ViewTypes.COUNT;
458     }
459 
460     @Override
getItemViewType(int position)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      */
getFrequentHeaderPosition()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 
ContactTileRow(Context context, int itemViewType)514         public ContactTileRow(Context context, int itemViewType) {
515             super(context);
516             mItemViewType = itemViewType;
517             mLayoutResId = getLayoutResourceId(mItemViewType);
518         }
519 
520         /**
521          * Configures the row to add {@link ContactEntry}s information to the views
522          */
configureRow(ArrayList<ContactEntry> list, boolean isLastRow)523         public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) {
524             int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount;
525 
526             // Adding tiles to row and filling in contact information
527             for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) {
528                 ContactEntry entry =
529                         columnCounter < list.size() ? list.get(columnCounter) : null;
530                 addTileFromEntry(entry, columnCounter, isLastRow);
531             }
532         }
533 
534         private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) {
535             final ContactTileView contactTile;
536 
537             if (getChildCount() <= childIndex) {
538                 contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null);
539                 // Note: the layoutparam set here is only actually used for FREQUENT.
540                 // We override onMeasure() for STARRED and we don't care the layout param there.
541                 Resources resources = mContext.getResources();
542                 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
543                         ViewGroup.LayoutParams.WRAP_CONTENT,
544                         ViewGroup.LayoutParams.WRAP_CONTENT);
545                 params.setMargins(
546                         resources.getDimensionPixelSize(R.dimen.detail_item_side_margin),
547                         0,
548                         resources.getDimensionPixelSize(R.dimen.detail_item_side_margin),
549                         0);
550                 contactTile.setLayoutParams(params);
551                 contactTile.setPhotoManager(mPhotoManager);
552                 contactTile.setListener(mListener);
553                 addView(contactTile);
554             } else {
555                 contactTile = (ContactTileView) getChildAt(childIndex);
556             }
557             contactTile.loadFromContact(entry);
558 
559             switch (mItemViewType) {
560                 case ViewTypes.STARRED_PHONE:
561                 case ViewTypes.STARRED:
562                     // Setting divider visibilities
563                     contactTile.setPadding(0, 0,
564                             childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels,
565                             isLastRow ? 0 : mPaddingInPixels);
566                     break;
567                 case ViewTypes.FREQUENT:
568                     contactTile.setHorizontalDividerVisibility(
569                             isLastRow ? View.GONE : View.VISIBLE);
570                     break;
571                 default:
572                     break;
573             }
574         }
575 
576         @Override
577         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
578             switch (mItemViewType) {
579                 case ViewTypes.STARRED_PHONE:
580                 case ViewTypes.STARRED:
581                     onLayoutForTiles();
582                     return;
583                 default:
584                     super.onLayout(changed, left, top, right, bottom);
585                     return;
586             }
587         }
588 
589         private void onLayoutForTiles() {
590             final int count = getChildCount();
591 
592             // Just line up children horizontally.
593             int childLeft = 0;
594             for (int i = 0; i < count; i++) {
595                 final View child = getChildAt(i);
596 
597                 // Note MeasuredWidth includes the padding.
598                 final int childWidth = child.getMeasuredWidth();
599                 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
600                 childLeft += childWidth;
601             }
602         }
603 
604         @Override
605         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
606             switch (mItemViewType) {
607                 case ViewTypes.STARRED_PHONE:
608                 case ViewTypes.STARRED:
609                     onMeasureForTiles(widthMeasureSpec);
610                     return;
611                 default:
612                     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
613                     return;
614             }
615         }
616 
617         private void onMeasureForTiles(int widthMeasureSpec) {
618             final int width = MeasureSpec.getSize(widthMeasureSpec);
619 
620             final int childCount = getChildCount();
621             if (childCount == 0) {
622                 // Just in case...
623                 setMeasuredDimension(width, 0);
624                 return;
625             }
626 
627             // 1. Calculate image size.
628             //      = ([total width] - [total padding]) / [child count]
629             //
630             // 2. Set it to width/height of each children.
631             //    If we have a remainder, some tiles will have 1 pixel larger width than its height.
632             //
633             // 3. Set the dimensions of itself.
634             //    Let width = given width.
635             //    Let height = image size + bottom paddding.
636 
637             final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels;
638 
639             // Preferred width / height for images (excluding the padding).
640             // The actual width may be 1 pixel larger than this if we have a remainder.
641             final int imageSize = (width - totalPaddingsInPixels) / mColumnCount;
642             final int remainder = width - (imageSize * mColumnCount) - totalPaddingsInPixels;
643 
644             for (int i = 0; i < childCount; i++) {
645                 final View child = getChildAt(i);
646                 final int childWidth = imageSize + child.getPaddingRight()
647                         // Compensate for the remainder
648                         + (i < remainder ? 1 : 0);
649                 final int childHeight = imageSize + child.getPaddingBottom();
650                 child.measure(
651                         MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
652                         MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
653                         );
654             }
655             setMeasuredDimension(width, imageSize + getChildAt(0).getPaddingBottom());
656         }
657 
658         @Override
659         public void sendAccessibilityEvent(int eventType) {
660             // This method is called when the child tile is INVISIBLE (meaning "empty"), and the
661             // Accessibility Manager needs to find alternative content description to speak.
662             // Here, we ignore the default behavior, since we don't want to let the manager speak
663             // a contact name for the tile next to the INVISIBLE tile.
664         }
665     }
666 
667     /**
668      * Class to hold contact information
669      */
670     public static class ContactEntry {
671         public String name;
672         public String status;
673         public String phoneLabel;
674         public String phoneNumber;
675         public Uri photoUri;
676         public Uri lookupKey;
677         public Drawable presenceIcon;
678     }
679 
680     private static class ViewTypes {
681         public static final int COUNT = 4;
682         public static final int STARRED = 0;
683         public static final int DIVIDER = 1;
684         public static final int FREQUENT = 2;
685         public static final int STARRED_PHONE = 3;
686     }
687 }
688