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  */
16 package com.android.contacts.common.list;
17 
18 import android.content.Context;
19 import android.content.CursorLoader;
20 import android.content.res.Resources;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.os.Bundle;
24 import android.provider.ContactsContract;
25 import android.provider.ContactsContract.Contacts;
26 import android.provider.ContactsContract.Data;
27 import android.provider.ContactsContract.Directory;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.QuickContactBadge;
34 import android.widget.SectionIndexer;
35 import android.widget.TextView;
36 
37 import com.android.contacts.common.ContactPhotoManager;
38 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
39 import com.android.contacts.common.R;
40 import com.android.contacts.common.util.SearchUtil;
41 
42 import java.util.HashSet;
43 
44 /**
45  * Common base class for various contact-related lists, e.g. contact list, phone number list
46  * etc.
47  */
48 public abstract class ContactEntryListAdapter extends IndexerListAdapter {
49 
50     private static final String TAG = "ContactEntryListAdapter";
51 
52     /**
53      * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
54      * be included in the search.
55      */
56     public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
57 
58     private int mDisplayOrder;
59     private int mSortOrder;
60 
61     private boolean mDisplayPhotos;
62     private boolean mCircularPhotos = true;
63     private boolean mQuickContactEnabled;
64     private boolean mAdjustSelectionBoundsEnabled;
65 
66     /**
67      * indicates if contact queries include profile
68      */
69     private boolean mIncludeProfile;
70 
71     /**
72      * indicates if query results includes a profile
73      */
74     private boolean mProfileExists;
75 
76     /**
77      * The root view of the fragment that this adapter is associated with.
78      */
79     private View mFragmentRootView;
80 
81     private ContactPhotoManager mPhotoLoader;
82 
83     private String mQueryString;
84     private String mUpperCaseQueryString;
85     private boolean mSearchMode;
86     private int mDirectorySearchMode;
87     private int mDirectoryResultLimit = Integer.MAX_VALUE;
88 
89     private boolean mEmptyListEnabled = true;
90 
91     private boolean mSelectionVisible;
92 
93     private ContactListFilter mFilter;
94     private boolean mDarkTheme = false;
95 
96     /** Resource used to provide header-text for default filter. */
97     private CharSequence mDefaultFilterHeaderText;
98 
ContactEntryListAdapter(Context context)99     public ContactEntryListAdapter(Context context) {
100         super(context);
101         setDefaultFilterHeaderText(R.string.local_search_label);
102         addPartitions();
103     }
104 
105     /**
106      * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of
107      * image loading requests that get cancelled on cursor changes.
108      */
setFragmentRootView(View fragmentRootView)109     protected void setFragmentRootView(View fragmentRootView) {
110         mFragmentRootView = fragmentRootView;
111     }
112 
setDefaultFilterHeaderText(int resourceId)113     protected void setDefaultFilterHeaderText(int resourceId) {
114         mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
115     }
116 
117     @Override
newView( Context context, int partition, Cursor cursor, int position, ViewGroup parent)118     protected ContactListItemView newView(
119             Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
120         final ContactListItemView view = new ContactListItemView(context, null);
121         view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
122         view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
123         return view;
124     }
125 
126     @Override
bindView(View itemView, int partition, Cursor cursor, int position)127     protected void bindView(View itemView, int partition, Cursor cursor, int position) {
128         final ContactListItemView view = (ContactListItemView) itemView;
129         view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
130     }
131 
132     @Override
createPinnedSectionHeaderView(Context context, ViewGroup parent)133     protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
134         return new ContactListPinnedHeaderView(context, null, parent);
135     }
136 
137     @Override
setPinnedSectionTitle(View pinnedHeaderView, String title)138     protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
139         ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
140     }
141 
addPartitions()142     protected void addPartitions() {
143         addPartition(createDefaultDirectoryPartition());
144     }
145 
createDefaultDirectoryPartition()146     protected DirectoryPartition createDefaultDirectoryPartition() {
147         DirectoryPartition partition = new DirectoryPartition(true, true);
148         partition.setDirectoryId(Directory.DEFAULT);
149         partition.setDirectoryType(getContext().getString(R.string.contactsList));
150         partition.setPriorityDirectory(true);
151         partition.setPhotoSupported(true);
152         partition.setLabel(mDefaultFilterHeaderText.toString());
153         return partition;
154     }
155 
156     /**
157      * Remove all directories after the default directory. This is typically used when contacts
158      * list screens are asked to exit the search mode and thus need to remove all remote directory
159      * results for the search.
160      *
161      * This code assumes that the default directory and directories before that should not be
162      * deleted (e.g. Join screen has "suggested contacts" directory before the default director,
163      * and we should not remove the directory).
164      */
removeDirectoriesAfterDefault()165     public void removeDirectoriesAfterDefault() {
166         final int partitionCount = getPartitionCount();
167         for (int i = partitionCount - 1; i >= 0; i--) {
168             final Partition partition = getPartition(i);
169             if ((partition instanceof DirectoryPartition)
170                     && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
171                 break;
172             } else {
173                 removePartition(i);
174             }
175         }
176     }
177 
getPartitionByDirectoryId(long id)178     protected int getPartitionByDirectoryId(long id) {
179         int count = getPartitionCount();
180         for (int i = 0; i < count; i++) {
181             Partition partition = getPartition(i);
182             if (partition instanceof DirectoryPartition) {
183                 if (((DirectoryPartition)partition).getDirectoryId() == id) {
184                     return i;
185                 }
186             }
187         }
188         return -1;
189     }
190 
getDirectoryById(long id)191     protected DirectoryPartition getDirectoryById(long id) {
192         int count = getPartitionCount();
193         for (int i = 0; i < count; i++) {
194             Partition partition = getPartition(i);
195             if (partition instanceof DirectoryPartition) {
196                 final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
197                 if (directoryPartition.getDirectoryId() == id) {
198                     return directoryPartition;
199                 }
200             }
201         }
202         return null;
203     }
204 
getContactDisplayName(int position)205     public abstract String getContactDisplayName(int position);
configureLoader(CursorLoader loader, long directoryId)206     public abstract void configureLoader(CursorLoader loader, long directoryId);
207 
208     /**
209      * Marks all partitions as "loading"
210      */
onDataReload()211     public void onDataReload() {
212         boolean notify = false;
213         int count = getPartitionCount();
214         for (int i = 0; i < count; i++) {
215             Partition partition = getPartition(i);
216             if (partition instanceof DirectoryPartition) {
217                 DirectoryPartition directoryPartition = (DirectoryPartition)partition;
218                 if (!directoryPartition.isLoading()) {
219                     notify = true;
220                 }
221                 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
222             }
223         }
224         if (notify) {
225             notifyDataSetChanged();
226         }
227     }
228 
229     @Override
clearPartitions()230     public void clearPartitions() {
231         int count = getPartitionCount();
232         for (int i = 0; i < count; i++) {
233             Partition partition = getPartition(i);
234             if (partition instanceof DirectoryPartition) {
235                 DirectoryPartition directoryPartition = (DirectoryPartition)partition;
236                 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
237             }
238         }
239         super.clearPartitions();
240     }
241 
isSearchMode()242     public boolean isSearchMode() {
243         return mSearchMode;
244     }
245 
setSearchMode(boolean flag)246     public void setSearchMode(boolean flag) {
247         mSearchMode = flag;
248     }
249 
getQueryString()250     public String getQueryString() {
251         return mQueryString;
252     }
253 
setQueryString(String queryString)254     public void setQueryString(String queryString) {
255         mQueryString = queryString;
256         if (TextUtils.isEmpty(queryString)) {
257             mUpperCaseQueryString = null;
258         } else {
259             mUpperCaseQueryString = SearchUtil
260                     .cleanStartAndEndOfSearchQuery(queryString.toUpperCase()) ;
261         }
262     }
263 
getUpperCaseQueryString()264     public String getUpperCaseQueryString() {
265         return mUpperCaseQueryString;
266     }
267 
getDirectorySearchMode()268     public int getDirectorySearchMode() {
269         return mDirectorySearchMode;
270     }
271 
setDirectorySearchMode(int mode)272     public void setDirectorySearchMode(int mode) {
273         mDirectorySearchMode = mode;
274     }
275 
getDirectoryResultLimit()276     public int getDirectoryResultLimit() {
277         return mDirectoryResultLimit;
278     }
279 
getDirectoryResultLimit(DirectoryPartition directoryPartition)280     public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
281         final int limit = directoryPartition.getResultLimit();
282         return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
283     }
284 
setDirectoryResultLimit(int limit)285     public void setDirectoryResultLimit(int limit) {
286         this.mDirectoryResultLimit = limit;
287     }
288 
getContactNameDisplayOrder()289     public int getContactNameDisplayOrder() {
290         return mDisplayOrder;
291     }
292 
setContactNameDisplayOrder(int displayOrder)293     public void setContactNameDisplayOrder(int displayOrder) {
294         mDisplayOrder = displayOrder;
295     }
296 
getSortOrder()297     public int getSortOrder() {
298         return mSortOrder;
299     }
300 
setSortOrder(int sortOrder)301     public void setSortOrder(int sortOrder) {
302         mSortOrder = sortOrder;
303     }
304 
setPhotoLoader(ContactPhotoManager photoLoader)305     public void setPhotoLoader(ContactPhotoManager photoLoader) {
306         mPhotoLoader = photoLoader;
307     }
308 
getPhotoLoader()309     protected ContactPhotoManager getPhotoLoader() {
310         return mPhotoLoader;
311     }
312 
getDisplayPhotos()313     public boolean getDisplayPhotos() {
314         return mDisplayPhotos;
315     }
316 
setDisplayPhotos(boolean displayPhotos)317     public void setDisplayPhotos(boolean displayPhotos) {
318         mDisplayPhotos = displayPhotos;
319     }
320 
getCircularPhotos()321     public boolean getCircularPhotos() {
322         return mCircularPhotos;
323     }
324 
setCircularPhotos(boolean circularPhotos)325     public void setCircularPhotos(boolean circularPhotos) {
326         mCircularPhotos = circularPhotos;
327     }
328 
isEmptyListEnabled()329     public boolean isEmptyListEnabled() {
330         return mEmptyListEnabled;
331     }
332 
setEmptyListEnabled(boolean flag)333     public void setEmptyListEnabled(boolean flag) {
334         mEmptyListEnabled = flag;
335     }
336 
isSelectionVisible()337     public boolean isSelectionVisible() {
338         return mSelectionVisible;
339     }
340 
setSelectionVisible(boolean flag)341     public void setSelectionVisible(boolean flag) {
342         this.mSelectionVisible = flag;
343     }
344 
isQuickContactEnabled()345     public boolean isQuickContactEnabled() {
346         return mQuickContactEnabled;
347     }
348 
setQuickContactEnabled(boolean quickContactEnabled)349     public void setQuickContactEnabled(boolean quickContactEnabled) {
350         mQuickContactEnabled = quickContactEnabled;
351     }
352 
isAdjustSelectionBoundsEnabled()353     public boolean isAdjustSelectionBoundsEnabled() {
354         return mAdjustSelectionBoundsEnabled;
355     }
356 
setAdjustSelectionBoundsEnabled(boolean enabled)357     public void setAdjustSelectionBoundsEnabled(boolean enabled) {
358         mAdjustSelectionBoundsEnabled = enabled;
359     }
360 
shouldIncludeProfile()361     public boolean shouldIncludeProfile() {
362         return mIncludeProfile;
363     }
364 
setIncludeProfile(boolean includeProfile)365     public void setIncludeProfile(boolean includeProfile) {
366         mIncludeProfile = includeProfile;
367     }
368 
setProfileExists(boolean exists)369     public void setProfileExists(boolean exists) {
370         mProfileExists = exists;
371         // Stick the "ME" header for the profile
372         if (exists) {
373             SectionIndexer indexer = getIndexer();
374             if (indexer != null) {
375                 ((ContactsSectionIndexer) indexer).setProfileHeader(
376                         getContext().getString(R.string.user_profile_contacts_list_header));
377             }
378         }
379     }
380 
hasProfile()381     public boolean hasProfile() {
382         return mProfileExists;
383     }
384 
setDarkTheme(boolean value)385     public void setDarkTheme(boolean value) {
386         mDarkTheme = value;
387     }
388 
389     /**
390      * Updates partitions according to the directory meta-data contained in the supplied
391      * cursor.
392      */
changeDirectories(Cursor cursor)393     public void changeDirectories(Cursor cursor) {
394         if (cursor.getCount() == 0) {
395             // Directory table must have at least local directory, without which this adapter will
396             // enter very weird state.
397             Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " +
398                     "no directory entries.", new RuntimeException());
399             return;
400         }
401         HashSet<Long> directoryIds = new HashSet<Long>();
402 
403         int idColumnIndex = cursor.getColumnIndex(Directory._ID);
404         int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
405         int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
406         int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
407 
408         // TODO preserve the order of partition to match those of the cursor
409         // Phase I: add new directories
410         cursor.moveToPosition(-1);
411         while (cursor.moveToNext()) {
412             long id = cursor.getLong(idColumnIndex);
413             directoryIds.add(id);
414             if (getPartitionByDirectoryId(id) == -1) {
415                 DirectoryPartition partition = new DirectoryPartition(false, true);
416                 partition.setDirectoryId(id);
417                 if (isRemoteDirectory(id)) {
418                     partition.setLabel(mContext.getString(R.string.directory_search_label));
419                 } else {
420                     partition.setLabel(mDefaultFilterHeaderText.toString());
421                 }
422                 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
423                 partition.setDisplayName(cursor.getString(displayNameColumnIndex));
424                 int photoSupport = cursor.getInt(photoSupportColumnIndex);
425                 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
426                         || photoSupport == Directory.PHOTO_SUPPORT_FULL);
427                 addPartition(partition);
428             }
429         }
430 
431         // Phase II: remove deleted directories
432         int count = getPartitionCount();
433         for (int i = count; --i >= 0; ) {
434             Partition partition = getPartition(i);
435             if (partition instanceof DirectoryPartition) {
436                 long id = ((DirectoryPartition)partition).getDirectoryId();
437                 if (!directoryIds.contains(id)) {
438                     removePartition(i);
439                 }
440             }
441         }
442 
443         invalidate();
444         notifyDataSetChanged();
445     }
446 
447     @Override
changeCursor(int partitionIndex, Cursor cursor)448     public void changeCursor(int partitionIndex, Cursor cursor) {
449         if (partitionIndex >= getPartitionCount()) {
450             // There is no partition for this data
451             return;
452         }
453 
454         Partition partition = getPartition(partitionIndex);
455         if (partition instanceof DirectoryPartition) {
456             ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED);
457         }
458 
459         if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
460             mPhotoLoader.refreshCache();
461         }
462 
463         super.changeCursor(partitionIndex, cursor);
464 
465         if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
466             updateIndexer(cursor);
467         }
468 
469         // When the cursor changes, cancel any pending asynchronous photo loads.
470         mPhotoLoader.cancelPendingRequests(mFragmentRootView);
471     }
472 
changeCursor(Cursor cursor)473     public void changeCursor(Cursor cursor) {
474         changeCursor(0, cursor);
475     }
476 
477     /**
478      * Updates the indexer, which is used to produce section headers.
479      */
updateIndexer(Cursor cursor)480     private void updateIndexer(Cursor cursor) {
481         if (cursor == null) {
482             setIndexer(null);
483             return;
484         }
485 
486         Bundle bundle = cursor.getExtras();
487         if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) &&
488                 bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) {
489             String sections[] =
490                     bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
491             int counts[] = bundle.getIntArray(
492                     Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
493 
494             if (getExtraStartingSection()) {
495                 // Insert an additional unnamed section at the top of the list.
496                 String allSections[] = new String[sections.length + 1];
497                 int allCounts[] = new int[counts.length + 1];
498                 for (int i = 0; i < sections.length; i++) {
499                     allSections[i + 1] = sections[i];
500                     allCounts[i + 1] = counts[i];
501                 }
502                 allCounts[0] = 1;
503                 allSections[0] = "";
504                 setIndexer(new ContactsSectionIndexer(allSections, allCounts));
505             } else {
506                 setIndexer(new ContactsSectionIndexer(sections, counts));
507             }
508         } else {
509             setIndexer(null);
510         }
511     }
512 
getExtraStartingSection()513     protected boolean getExtraStartingSection() {
514         return false;
515     }
516 
517     @Override
getViewTypeCount()518     public int getViewTypeCount() {
519         // We need a separate view type for each item type, plus another one for
520         // each type with header, plus one for "other".
521         return getItemViewTypeCount() * 2 + 1;
522     }
523 
524     @Override
getItemViewType(int partitionIndex, int position)525     public int getItemViewType(int partitionIndex, int position) {
526         int type = super.getItemViewType(partitionIndex, position);
527         if (!isUserProfile(position)
528                 && isSectionHeaderDisplayEnabled()
529                 && partitionIndex == getIndexedPartition()) {
530             Placement placement = getItemPlacementInSection(position);
531             return placement.firstInSection ? type : getItemViewTypeCount() + type;
532         } else {
533             return type;
534         }
535     }
536 
537     @Override
isEmpty()538     public boolean isEmpty() {
539         // TODO
540 //        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
541 //            return true;
542 //        }
543 
544         if (!mEmptyListEnabled) {
545             return false;
546         } else if (isSearchMode()) {
547             return TextUtils.isEmpty(getQueryString());
548         } else {
549             return super.isEmpty();
550         }
551     }
552 
isLoading()553     public boolean isLoading() {
554         int count = getPartitionCount();
555         for (int i = 0; i < count; i++) {
556             Partition partition = getPartition(i);
557             if (partition instanceof DirectoryPartition
558                     && ((DirectoryPartition) partition).isLoading()) {
559                 return true;
560             }
561         }
562         return false;
563     }
564 
areAllPartitionsEmpty()565     public boolean areAllPartitionsEmpty() {
566         int count = getPartitionCount();
567         for (int i = 0; i < count; i++) {
568             if (!isPartitionEmpty(i)) {
569                 return false;
570             }
571         }
572         return true;
573     }
574 
575     /**
576      * Changes visibility parameters for the default directory partition.
577      */
configureDefaultPartition(boolean showIfEmpty, boolean hasHeader)578     public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
579         int defaultPartitionIndex = -1;
580         int count = getPartitionCount();
581         for (int i = 0; i < count; i++) {
582             Partition partition = getPartition(i);
583             if (partition instanceof DirectoryPartition &&
584                     ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
585                 defaultPartitionIndex = i;
586                 break;
587             }
588         }
589         if (defaultPartitionIndex != -1) {
590             setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
591             setHasHeader(defaultPartitionIndex, hasHeader);
592         }
593     }
594 
595     @Override
newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent)596     protected View newHeaderView(Context context, int partition, Cursor cursor,
597             ViewGroup parent) {
598         LayoutInflater inflater = LayoutInflater.from(context);
599         View view = inflater.inflate(R.layout.directory_header, parent, false);
600         if (!getPinnedPartitionHeadersEnabled()) {
601             // If the headers are unpinned, there is no need for their background
602             // color to be non-transparent. Setting this transparent reduces maintenance for
603             // non-pinned headers. We don't need to bother synchronizing the activity's
604             // background color with the header background color.
605             view.setBackground(null);
606         }
607         return view;
608     }
609 
610     @Override
bindHeaderView(View view, int partitionIndex, Cursor cursor)611     protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
612         Partition partition = getPartition(partitionIndex);
613         if (!(partition instanceof DirectoryPartition)) {
614             return;
615         }
616 
617         DirectoryPartition directoryPartition = (DirectoryPartition)partition;
618         long directoryId = directoryPartition.getDirectoryId();
619         TextView labelTextView = (TextView)view.findViewById(R.id.label);
620         TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
621         labelTextView.setText(directoryPartition.getLabel());
622         if (!isRemoteDirectory(directoryId)) {
623             displayNameTextView.setText(null);
624         } else {
625             String directoryName = directoryPartition.getDisplayName();
626             String displayName = !TextUtils.isEmpty(directoryName)
627                     ? directoryName
628                     : directoryPartition.getDirectoryType();
629             displayNameTextView.setText(displayName);
630         }
631 
632         final Resources res = getContext().getResources();
633         final int headerPaddingTop = partitionIndex == 1 && getPartition(0).isEmpty()?
634                 0 : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding);
635         // There should be no extra padding at the top of the first directory header
636         view.setPaddingRelative(view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(),
637                 view.getPaddingBottom());
638     }
639 
640     // Default implementation simply returns number of rows in the cursor.
641     // Broken out into its own routine so can be overridden by child classes
642     // for eg number of unique contacts for a phone list.
getResultCount(Cursor cursor)643     protected int getResultCount(Cursor cursor) {
644         return cursor == null ? 0 : cursor.getCount();
645     }
646 
647     /**
648      * Checks whether the contact entry at the given position represents the user's profile.
649      */
isUserProfile(int position)650     protected boolean isUserProfile(int position) {
651         // The profile only ever appears in the first position if it is present.  So if the position
652         // is anything beyond 0, it can't be the profile.
653         boolean isUserProfile = false;
654         if (position == 0) {
655             int partition = getPartitionForPosition(position);
656             if (partition >= 0) {
657                 // Save the old cursor position - the call to getItem() may modify the cursor
658                 // position.
659                 int offset = getCursor(partition).getPosition();
660                 Cursor cursor = (Cursor) getItem(position);
661                 if (cursor != null) {
662                     int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
663                     if (profileColumnIndex != -1) {
664                         isUserProfile = cursor.getInt(profileColumnIndex) == 1;
665                     }
666                     // Restore the old cursor position.
667                     cursor.moveToPosition(offset);
668                 }
669             }
670         }
671         return isUserProfile;
672     }
673 
674     // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
getQuantityText(int count, int zeroResourceId, int pluralResourceId)675     public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
676         if (count == 0) {
677             return getContext().getString(zeroResourceId);
678         } else {
679             String format = getContext().getResources()
680                     .getQuantityText(pluralResourceId, count).toString();
681             return String.format(format, count);
682         }
683     }
684 
isPhotoSupported(int partitionIndex)685     public boolean isPhotoSupported(int partitionIndex) {
686         Partition partition = getPartition(partitionIndex);
687         if (partition instanceof DirectoryPartition) {
688             return ((DirectoryPartition) partition).isPhotoSupported();
689         }
690         return true;
691     }
692 
693     /**
694      * Returns the currently selected filter.
695      */
getFilter()696     public ContactListFilter getFilter() {
697         return mFilter;
698     }
699 
setFilter(ContactListFilter filter)700     public void setFilter(ContactListFilter filter) {
701         mFilter = filter;
702     }
703 
704     // TODO: move sharable logic (bindXX() methods) to here with extra arguments
705 
706     /**
707      * Loads the photo for the quick contact view and assigns the contact uri.
708      * @param photoIdColumn Index of the photo id column
709      * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
710      * @param contactIdColumn Index of the contact id column
711      * @param lookUpKeyColumn Index of the lookup key column
712      * @param displayNameColumn Index of the display name column
713      */
bindQuickContact(final ContactListItemView view, int partitionIndex, Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, int lookUpKeyColumn, int displayNameColumn)714     protected void bindQuickContact(final ContactListItemView view, int partitionIndex,
715             Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn,
716             int lookUpKeyColumn, int displayNameColumn) {
717         long photoId = 0;
718         if (!cursor.isNull(photoIdColumn)) {
719             photoId = cursor.getLong(photoIdColumn);
720         }
721 
722         QuickContactBadge quickContact = view.getQuickContact();
723         quickContact.assignContactUri(
724                 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
725 
726         if (photoId != 0 || photoUriColumn == -1) {
727             getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos,
728                     null);
729         } else {
730             final String photoUriString = cursor.getString(photoUriColumn);
731             final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
732             DefaultImageRequest request = null;
733             if (photoUri == null) {
734                 request = getDefaultImageRequestFromCursor(cursor, displayNameColumn,
735                         lookUpKeyColumn);
736             }
737             getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos,
738                     request);
739         }
740 
741     }
742 
743     @Override
hasStableIds()744     public boolean hasStableIds() {
745         // Whenever bindViewId() is called, the values passed into setId() are stable or
746         // stable-ish. For example, when one contact is modified we don't expect a second
747         // contact's Contact._ID values to change.
748         return true;
749     }
750 
bindViewId(final ContactListItemView view, Cursor cursor, int idColumn)751     protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) {
752         // Set a semi-stable id, so that talkback won't get confused when the list gets
753         // refreshed. There is little harm in inserting the same ID twice.
754         long contactId = cursor.getLong(idColumn);
755         view.setId((int) (contactId % Integer.MAX_VALUE));
756 
757     }
758 
getContactUri(int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn)759     protected Uri getContactUri(int partitionIndex, Cursor cursor,
760             int contactIdColumn, int lookUpKeyColumn) {
761         long contactId = cursor.getLong(contactIdColumn);
762         String lookupKey = cursor.getString(lookUpKeyColumn);
763         long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
764         // Remote directories must have a lookup key or we don't have
765         // a working contact URI
766         if (TextUtils.isEmpty(lookupKey) && isRemoteDirectory(directoryId)) {
767             return null;
768         }
769         Uri uri = Contacts.getLookupUri(contactId, lookupKey);
770         if (directoryId != Directory.DEFAULT) {
771             uri = uri.buildUpon().appendQueryParameter(
772                     ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
773         }
774         return uri;
775     }
776 
isRemoteDirectory(long directoryId)777     public static boolean isRemoteDirectory(long directoryId) {
778         return directoryId != Directory.DEFAULT
779                 && directoryId != Directory.LOCAL_INVISIBLE;
780     }
781 
782     /**
783      * Retrieves the lookup key and display name from a cursor, and returns a
784      * {@link DefaultImageRequest} containing these contact details
785      *
786      * @param cursor Contacts cursor positioned at the current row to retrieve contact details for
787      * @param displayNameColumn Column index of the display name
788      * @param lookupKeyColumn Column index of the lookup key
789      * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the
790      * display name and lookup key of the contact.
791      */
getDefaultImageRequestFromCursor(Cursor cursor, int displayNameColumn, int lookupKeyColumn)792     public DefaultImageRequest getDefaultImageRequestFromCursor(Cursor cursor,
793             int displayNameColumn, int lookupKeyColumn) {
794         final String displayName = cursor.getString(displayNameColumn);
795         final String lookupKey = cursor.getString(lookupKeyColumn);
796         return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
797     }
798 }
799