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 */
16package com.android.contacts.common.list;
17
18import android.content.ContentUris;
19import android.content.Context;
20import android.content.res.Resources;
21import android.database.Cursor;
22import android.graphics.drawable.Drawable;
23import android.net.Uri;
24import android.provider.ContactsContract.Contacts;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.BaseAdapter;
28import android.widget.FrameLayout;
29import android.widget.TextView;
30
31import com.android.contacts.common.ContactPhotoManager;
32import com.android.contacts.common.ContactPresenceIconUtil;
33import com.android.contacts.common.ContactStatusUtil;
34import com.android.contacts.common.ContactTileLoaderFactory;
35import com.android.contacts.common.MoreContactUtils;
36import com.android.contacts.common.R;
37import com.android.contacts.common.util.ViewUtil;
38
39import 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 */
45public 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
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
124    public void setPhotoLoader(ContactPhotoManager photoLoader) {
125        mPhotoManager = photoLoader;
126    }
127
128    public void setColumnCount(int columnCount) {
129        mColumnCount = columnCount;
130    }
131
132    public void setDisplayType(DisplayType displayType) {
133        mDisplayType = displayType;
134    }
135
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     */
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
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     */
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     */
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     */
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
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.name = (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     */
280    public int getNumFrequents() {
281        return mNumFrequents;
282    }
283
284    @Override
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     */
315    protected int getRowCount(int entryCount) {
316        return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1;
317    }
318
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
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
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
375    public boolean areAllItemsEnabled() {
376        return (mDisplayType != DisplayType.STREQUENT);
377    }
378
379    @Override
380    public boolean isEnabled(int position) {
381        return position != getRowCount(mDividerPosition);
382    }
383
384    @Override
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     */
411    private TextView getDivider() {
412        return MoreContactUtils.createHeaderView(mContext, R.string.favoritesFrequentContacted);
413    }
414
415    private void setDividerPadding(TextView headerTextView, boolean isFirstRow) {
416        MoreContactUtils.setHeaderViewBottomPadding(mContext, headerTextView, isFirstRow);
417    }
418
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
431    public int getViewTypeCount() {
432        return ViewTypes.COUNT;
433    }
434
435    @Override
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     */
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
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         */
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