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