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