ContactEntryListAdapter.java revision bd80fd64b9ff94c9ffbdb843beb4b363bb209463
1/*
2 * Copyright (C) 2010 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.Context;
19import android.content.CursorLoader;
20import android.database.Cursor;
21import android.net.Uri;
22import android.os.Bundle;
23import android.provider.ContactsContract;
24import android.provider.ContactsContract.ContactCounts;
25import android.provider.ContactsContract.Contacts;
26import android.provider.ContactsContract.Directory;
27import android.text.TextUtils;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.QuickContactBadge;
33import android.widget.SectionIndexer;
34import android.widget.TextView;
35
36import com.android.contacts.common.ContactPhotoManager;
37import com.android.contacts.common.R;
38
39import java.util.HashSet;
40
41/**
42 * Common base class for various contact-related lists, e.g. contact list, phone number list
43 * etc.
44 */
45public abstract class ContactEntryListAdapter extends IndexerListAdapter {
46
47    private static final String TAG = "ContactEntryListAdapter";
48
49    /**
50     * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
51     * be included in the search.
52     */
53    public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
54
55    private int mDisplayOrder;
56    private int mSortOrder;
57
58    private boolean mDisplayPhotos;
59    private boolean mQuickContactEnabled;
60
61    /**
62     * indicates if contact queries include profile
63     */
64    private boolean mIncludeProfile;
65
66    /**
67     * indicates if query results includes a profile
68     */
69    private boolean mProfileExists;
70
71    private ContactPhotoManager mPhotoLoader;
72
73    private String mQueryString;
74    private String mUpperCaseQueryString;
75    private boolean mSearchMode;
76    private int mDirectorySearchMode;
77    private int mDirectoryResultLimit = Integer.MAX_VALUE;
78
79    private boolean mEmptyListEnabled = true;
80
81    private boolean mSelectionVisible;
82
83    private ContactListFilter mFilter;
84    private String mContactsCount = "";
85    private boolean mDarkTheme = false;
86
87    /** Resource used to provide header-text for default filter. */
88    private CharSequence mDefaultFilterHeaderText;
89
90    public ContactEntryListAdapter(Context context) {
91        super(context);
92        addPartitions();
93        setDefaultFilterHeaderText(R.string.local_search_label);
94    }
95
96    protected void setDefaultFilterHeaderText(int resourceId) {
97        mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
98    }
99
100    @Override
101    protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
102        return new ContactListPinnedHeaderView(context, null);
103    }
104
105    @Override
106    protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
107        ((ContactListPinnedHeaderView)pinnedHeaderView).setSectionHeader(title);
108    }
109
110    @Override
111    protected void setPinnedHeaderContactsCount(View header) {
112        // Update the header with the contacts count only if a profile header exists
113        // otherwise, the contacts count are shown in the empty profile header view
114        if (mProfileExists) {
115            ((ContactListPinnedHeaderView)header).setCountView(mContactsCount);
116        } else {
117            clearPinnedHeaderContactsCount(header);
118        }
119    }
120
121    @Override
122    protected void clearPinnedHeaderContactsCount(View header) {
123        ((ContactListPinnedHeaderView)header).setCountView(null);
124    }
125
126    protected void addPartitions() {
127        addPartition(createDefaultDirectoryPartition());
128    }
129
130    protected DirectoryPartition createDefaultDirectoryPartition() {
131        DirectoryPartition partition = new DirectoryPartition(true, true);
132        partition.setDirectoryId(Directory.DEFAULT);
133        partition.setDirectoryType(getContext().getString(R.string.contactsList));
134        partition.setPriorityDirectory(true);
135        partition.setPhotoSupported(true);
136        return partition;
137    }
138
139    /**
140     * Remove all directories after the default directory. This is typically used when contacts
141     * list screens are asked to exit the search mode and thus need to remove all remote directory
142     * results for the search.
143     *
144     * This code assumes that the default directory and directories before that should not be
145     * deleted (e.g. Join screen has "suggested contacts" directory before the default director,
146     * and we should not remove the directory).
147     */
148    public void removeDirectoriesAfterDefault() {
149        final int partitionCount = getPartitionCount();
150        for (int i = partitionCount - 1; i >= 0; i--) {
151            final Partition partition = getPartition(i);
152            if ((partition instanceof DirectoryPartition)
153                    && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
154                break;
155            } else {
156                removePartition(i);
157            }
158        }
159    }
160
161    private int getPartitionByDirectoryId(long id) {
162        int count = getPartitionCount();
163        for (int i = 0; i < count; i++) {
164            Partition partition = getPartition(i);
165            if (partition instanceof DirectoryPartition) {
166                if (((DirectoryPartition)partition).getDirectoryId() == id) {
167                    return i;
168                }
169            }
170        }
171        return -1;
172    }
173
174    public abstract String getContactDisplayName(int position);
175    public abstract void configureLoader(CursorLoader loader, long directoryId);
176
177    /**
178     * Marks all partitions as "loading"
179     */
180    public void onDataReload() {
181        boolean notify = false;
182        int count = getPartitionCount();
183        for (int i = 0; i < count; i++) {
184            Partition partition = getPartition(i);
185            if (partition instanceof DirectoryPartition) {
186                DirectoryPartition directoryPartition = (DirectoryPartition)partition;
187                if (!directoryPartition.isLoading()) {
188                    notify = true;
189                }
190                directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
191            }
192        }
193        if (notify) {
194            notifyDataSetChanged();
195        }
196    }
197
198    @Override
199    public void clearPartitions() {
200        int count = getPartitionCount();
201        for (int i = 0; i < count; i++) {
202            Partition partition = getPartition(i);
203            if (partition instanceof DirectoryPartition) {
204                DirectoryPartition directoryPartition = (DirectoryPartition)partition;
205                directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
206            }
207        }
208        super.clearPartitions();
209    }
210
211    public boolean isSearchMode() {
212        return mSearchMode;
213    }
214
215    public void setSearchMode(boolean flag) {
216        mSearchMode = flag;
217    }
218
219    public String getQueryString() {
220        return mQueryString;
221    }
222
223    public void setQueryString(String queryString) {
224        mQueryString = queryString;
225        if (TextUtils.isEmpty(queryString)) {
226            mUpperCaseQueryString = null;
227        } else {
228            mUpperCaseQueryString = queryString.toUpperCase();
229        }
230    }
231
232    public String getUpperCaseQueryString() {
233        return mUpperCaseQueryString;
234    }
235
236    public int getDirectorySearchMode() {
237        return mDirectorySearchMode;
238    }
239
240    public void setDirectorySearchMode(int mode) {
241        mDirectorySearchMode = mode;
242    }
243
244    public int getDirectoryResultLimit() {
245        return mDirectoryResultLimit;
246    }
247
248    public void setDirectoryResultLimit(int limit) {
249        this.mDirectoryResultLimit = limit;
250    }
251
252    public int getContactNameDisplayOrder() {
253        return mDisplayOrder;
254    }
255
256    public void setContactNameDisplayOrder(int displayOrder) {
257        mDisplayOrder = displayOrder;
258    }
259
260    public int getSortOrder() {
261        return mSortOrder;
262    }
263
264    public void setSortOrder(int sortOrder) {
265        mSortOrder = sortOrder;
266    }
267
268    public void setPhotoLoader(ContactPhotoManager photoLoader) {
269        mPhotoLoader = photoLoader;
270    }
271
272    protected ContactPhotoManager getPhotoLoader() {
273        return mPhotoLoader;
274    }
275
276    public boolean getDisplayPhotos() {
277        return mDisplayPhotos;
278    }
279
280    public void setDisplayPhotos(boolean displayPhotos) {
281        mDisplayPhotos = displayPhotos;
282    }
283
284    public boolean isEmptyListEnabled() {
285        return mEmptyListEnabled;
286    }
287
288    public void setEmptyListEnabled(boolean flag) {
289        mEmptyListEnabled = flag;
290    }
291
292    public boolean isSelectionVisible() {
293        return mSelectionVisible;
294    }
295
296    public void setSelectionVisible(boolean flag) {
297        this.mSelectionVisible = flag;
298    }
299
300    public boolean isQuickContactEnabled() {
301        return mQuickContactEnabled;
302    }
303
304    public void setQuickContactEnabled(boolean quickContactEnabled) {
305        mQuickContactEnabled = quickContactEnabled;
306    }
307
308    public boolean shouldIncludeProfile() {
309        return mIncludeProfile;
310    }
311
312    public void setIncludeProfile(boolean includeProfile) {
313        mIncludeProfile = includeProfile;
314    }
315
316    public void setProfileExists(boolean exists) {
317        mProfileExists = exists;
318        // Stick the "ME" header for the profile
319        if (exists) {
320            SectionIndexer indexer = getIndexer();
321            if (indexer != null) {
322                ((ContactsSectionIndexer) indexer).setProfileHeader(
323                        getContext().getString(R.string.user_profile_contacts_list_header));
324            }
325        }
326    }
327
328    public boolean hasProfile() {
329        return mProfileExists;
330    }
331
332    public void setDarkTheme(boolean value) {
333        mDarkTheme = value;
334    }
335
336    /**
337     * Updates partitions according to the directory meta-data contained in the supplied
338     * cursor.
339     */
340    public void changeDirectories(Cursor cursor) {
341        if (cursor.getCount() == 0) {
342            // Directory table must have at least local directory, without which this adapter will
343            // enter very weird state.
344            Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " +
345                    "no directory entries.", new RuntimeException());
346            return;
347        }
348        HashSet<Long> directoryIds = new HashSet<Long>();
349
350        int idColumnIndex = cursor.getColumnIndex(Directory._ID);
351        int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
352        int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
353        int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
354
355        // TODO preserve the order of partition to match those of the cursor
356        // Phase I: add new directories
357        cursor.moveToPosition(-1);
358        while (cursor.moveToNext()) {
359            long id = cursor.getLong(idColumnIndex);
360            directoryIds.add(id);
361            if (getPartitionByDirectoryId(id) == -1) {
362                DirectoryPartition partition = new DirectoryPartition(false, true);
363                partition.setDirectoryId(id);
364                partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
365                partition.setDisplayName(cursor.getString(displayNameColumnIndex));
366                int photoSupport = cursor.getInt(photoSupportColumnIndex);
367                partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
368                        || photoSupport == Directory.PHOTO_SUPPORT_FULL);
369                addPartition(partition);
370            }
371        }
372
373        // Phase II: remove deleted directories
374        int count = getPartitionCount();
375        for (int i = count; --i >= 0; ) {
376            Partition partition = getPartition(i);
377            if (partition instanceof DirectoryPartition) {
378                long id = ((DirectoryPartition)partition).getDirectoryId();
379                if (!directoryIds.contains(id)) {
380                    removePartition(i);
381                }
382            }
383        }
384
385        invalidate();
386        notifyDataSetChanged();
387    }
388
389    @Override
390    public void changeCursor(int partitionIndex, Cursor cursor) {
391        if (partitionIndex >= getPartitionCount()) {
392            // There is no partition for this data
393            return;
394        }
395
396        Partition partition = getPartition(partitionIndex);
397        if (partition instanceof DirectoryPartition) {
398            ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED);
399        }
400
401        if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
402            mPhotoLoader.refreshCache();
403        }
404
405        super.changeCursor(partitionIndex, cursor);
406
407        if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
408            updateIndexer(cursor);
409        }
410    }
411
412    public void changeCursor(Cursor cursor) {
413        changeCursor(0, cursor);
414    }
415
416    /**
417     * Updates the indexer, which is used to produce section headers.
418     */
419    private void updateIndexer(Cursor cursor) {
420        if (cursor == null) {
421            setIndexer(null);
422            return;
423        }
424
425        Bundle bundle = cursor.getExtras();
426        if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
427            String sections[] =
428                    bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
429            int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
430            setIndexer(new ContactsSectionIndexer(sections, counts));
431        } else {
432            setIndexer(null);
433        }
434    }
435
436    @Override
437    public int getViewTypeCount() {
438        // We need a separate view type for each item type, plus another one for
439        // each type with header, plus one for "other".
440        return getItemViewTypeCount() * 2 + 1;
441    }
442
443    @Override
444    public int getItemViewType(int partitionIndex, int position) {
445        int type = super.getItemViewType(partitionIndex, position);
446        if (!isUserProfile(position)
447                && isSectionHeaderDisplayEnabled()
448                && partitionIndex == getIndexedPartition()) {
449            Placement placement = getItemPlacementInSection(position);
450            return placement.firstInSection ? type : getItemViewTypeCount() + type;
451        } else {
452            return type;
453        }
454    }
455
456    @Override
457    public boolean isEmpty() {
458        // TODO
459//        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
460//            return true;
461//        }
462
463        if (!mEmptyListEnabled) {
464            return false;
465        } else if (isSearchMode()) {
466            return TextUtils.isEmpty(getQueryString());
467        } else {
468            return super.isEmpty();
469        }
470    }
471
472    public boolean isLoading() {
473        int count = getPartitionCount();
474        for (int i = 0; i < count; i++) {
475            Partition partition = getPartition(i);
476            if (partition instanceof DirectoryPartition
477                    && ((DirectoryPartition) partition).isLoading()) {
478                return true;
479            }
480        }
481        return false;
482    }
483
484    public boolean areAllPartitionsEmpty() {
485        int count = getPartitionCount();
486        for (int i = 0; i < count; i++) {
487            if (!isPartitionEmpty(i)) {
488                return false;
489            }
490        }
491        return true;
492    }
493
494    /**
495     * Changes visibility parameters for the default directory partition.
496     */
497    public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
498        int defaultPartitionIndex = -1;
499        int count = getPartitionCount();
500        for (int i = 0; i < count; i++) {
501            Partition partition = getPartition(i);
502            if (partition instanceof DirectoryPartition &&
503                    ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
504                defaultPartitionIndex = i;
505                break;
506            }
507        }
508        if (defaultPartitionIndex != -1) {
509            setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
510            setHasHeader(defaultPartitionIndex, hasHeader);
511        }
512    }
513
514    @Override
515    protected View newHeaderView(Context context, int partition, Cursor cursor,
516            ViewGroup parent) {
517        LayoutInflater inflater = LayoutInflater.from(context);
518        return inflater.inflate(R.layout.directory_header, parent, false);
519    }
520
521    @Override
522    protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
523        Partition partition = getPartition(partitionIndex);
524        if (!(partition instanceof DirectoryPartition)) {
525            return;
526        }
527
528        DirectoryPartition directoryPartition = (DirectoryPartition)partition;
529        long directoryId = directoryPartition.getDirectoryId();
530        TextView labelTextView = (TextView)view.findViewById(R.id.label);
531        TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
532        if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
533            labelTextView.setText(mDefaultFilterHeaderText);
534            displayNameTextView.setText(null);
535        } else {
536            labelTextView.setText(R.string.directory_search_label);
537            String directoryName = directoryPartition.getDisplayName();
538            String displayName = !TextUtils.isEmpty(directoryName)
539                    ? directoryName
540                    : directoryPartition.getDirectoryType();
541            displayNameTextView.setText(displayName);
542        }
543
544        TextView countText = (TextView)view.findViewById(R.id.count);
545        if (directoryPartition.isLoading()) {
546            countText.setText(R.string.search_results_searching);
547        } else {
548            int count = cursor == null ? 0 : cursor.getCount();
549            if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
550                    && count >= getDirectoryResultLimit()) {
551                countText.setText(mContext.getString(
552                        R.string.foundTooManyContacts, getDirectoryResultLimit()));
553            } else {
554                countText.setText(getQuantityText(
555                        count, R.string.listFoundAllContactsZero, R.plurals.searchFoundContacts));
556            }
557        }
558    }
559
560    /**
561     * Checks whether the contact entry at the given position represents the user's profile.
562     */
563    protected boolean isUserProfile(int position) {
564        // The profile only ever appears in the first position if it is present.  So if the position
565        // is anything beyond 0, it can't be the profile.
566        boolean isUserProfile = false;
567        if (position == 0) {
568            int partition = getPartitionForPosition(position);
569            if (partition >= 0) {
570                // Save the old cursor position - the call to getItem() may modify the cursor
571                // position.
572                int offset = getCursor(partition).getPosition();
573                Cursor cursor = (Cursor) getItem(position);
574                if (cursor != null) {
575                    int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
576                    if (profileColumnIndex != -1) {
577                        isUserProfile = cursor.getInt(profileColumnIndex) == 1;
578                    }
579                    // Restore the old cursor position.
580                    cursor.moveToPosition(offset);
581                }
582            }
583        }
584        return isUserProfile;
585    }
586
587    // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
588    public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
589        if (count == 0) {
590            return getContext().getString(zeroResourceId);
591        } else {
592            String format = getContext().getResources()
593                    .getQuantityText(pluralResourceId, count).toString();
594            return String.format(format, count);
595        }
596    }
597
598    public boolean isPhotoSupported(int partitionIndex) {
599        Partition partition = getPartition(partitionIndex);
600        if (partition instanceof DirectoryPartition) {
601            return ((DirectoryPartition) partition).isPhotoSupported();
602        }
603        return true;
604    }
605
606    /**
607     * Returns the currently selected filter.
608     */
609    public ContactListFilter getFilter() {
610        return mFilter;
611    }
612
613    public void setFilter(ContactListFilter filter) {
614        mFilter = filter;
615    }
616
617    // TODO: move sharable logic (bindXX() methods) to here with extra arguments
618
619    /**
620     * Loads the photo for the quick contact view and assigns the contact uri.
621     * @param photoIdColumn Index of the photo id column
622     * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
623     * @param contactIdColumn Index of the contact id column
624     * @param lookUpKeyColumn Index of the lookup key column
625     */
626    protected void bindQuickContact(final ContactListItemView view, int partitionIndex,
627            Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn,
628            int lookUpKeyColumn) {
629        long photoId = 0;
630        if (!cursor.isNull(photoIdColumn)) {
631            photoId = cursor.getLong(photoIdColumn);
632        }
633
634        QuickContactBadge quickContact = view.getQuickContact();
635        quickContact.assignContactUri(
636                getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
637
638        if (photoId != 0 || photoUriColumn == -1) {
639            getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme);
640        } else {
641            final String photoUriString = cursor.getString(photoUriColumn);
642            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
643            getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme);
644        }
645
646    }
647
648    protected Uri getContactUri(int partitionIndex, Cursor cursor,
649            int contactIdColumn, int lookUpKeyColumn) {
650        long contactId = cursor.getLong(contactIdColumn);
651        String lookupKey = cursor.getString(lookUpKeyColumn);
652        Uri uri = Contacts.getLookupUri(contactId, lookupKey);
653        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
654        if (directoryId != Directory.DEFAULT) {
655            uri = uri.buildUpon().appendQueryParameter(
656                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
657        }
658        return uri;
659    }
660
661    public void setContactsCount(String count) {
662        mContactsCount = count;
663    }
664
665    public String getContactsCount() {
666        return mContactsCount;
667    }
668}
669