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.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.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.ContactPhotoManager;
38 import com.android.contacts.ContactPhotoManager.DefaultImageRequest;
39 import com.android.contacts.ContactsUtils;
40 import com.android.contacts.R;
41 import com.android.contacts.compat.CompatUtils;
42 import com.android.contacts.compat.DirectoryCompat;
43 import com.android.contacts.util.SearchUtil;
44 
45 import java.util.HashSet;
46 
47 /**
48  * Common base class for various contact-related lists, e.g. contact list, phone number list
49  * etc.
50  */
51 public abstract class ContactEntryListAdapter extends IndexerListAdapter {
52 
53     private static final String TAG = "ContactEntryListAdapter";
54 
55     /**
56      * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
57      * be included in the search.
58      */
59     public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
60 
61     private int mDisplayOrder;
62     private int mSortOrder;
63 
64     private boolean mDisplayPhotos;
65     private boolean mCircularPhotos = true;
66     private boolean mQuickContactEnabled;
67     private boolean mAdjustSelectionBoundsEnabled;
68 
69     /**
70      * indicates if contact queries include favorites
71      */
72     private boolean mIncludeFavorites;
73 
74     private int mNumberOfFavorites;
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         bindWorkProfileIcon(view, partition);
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 
shouldIncludeFavorites()362     public boolean shouldIncludeFavorites() {
363         return mIncludeFavorites;
364     }
365 
setIncludeFavorites(boolean includeFavorites)366     public void setIncludeFavorites(boolean includeFavorites) {
367         mIncludeFavorites = includeFavorites;
368     }
369 
setFavoritesSectionHeader(int numberOfFavorites)370     public void setFavoritesSectionHeader(int numberOfFavorites) {
371         if (mIncludeFavorites) {
372             mNumberOfFavorites = numberOfFavorites;
373             setSectionHeader(numberOfFavorites);
374         }
375     }
376 
getNumberOfFavorites()377     public int getNumberOfFavorites() {
378         return mNumberOfFavorites;
379     }
380 
setSectionHeader(int numberOfItems)381     private void setSectionHeader(int numberOfItems) {
382         SectionIndexer indexer = getIndexer();
383         if (indexer != null) {
384             ((ContactsSectionIndexer) indexer).setFavoritesHeader(numberOfItems);
385         }
386     }
387 
setDarkTheme(boolean value)388     public void setDarkTheme(boolean value) {
389         mDarkTheme = value;
390     }
391 
392     /**
393      * Updates partitions according to the directory meta-data contained in the supplied
394      * cursor.
395      */
changeDirectories(Cursor cursor)396     public void changeDirectories(Cursor cursor) {
397         if (cursor.getCount() == 0) {
398             // Directory table must have at least local directory, without which this adapter will
399             // enter very weird state.
400             Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " +
401                     "no directory entries.", new RuntimeException());
402             return;
403         }
404         HashSet<Long> directoryIds = new HashSet<Long>();
405 
406         int idColumnIndex = cursor.getColumnIndex(Directory._ID);
407         int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
408         int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
409         int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
410 
411         // TODO preserve the order of partition to match those of the cursor
412         // Phase I: add new directories
413         cursor.moveToPosition(-1);
414         while (cursor.moveToNext()) {
415             long id = cursor.getLong(idColumnIndex);
416             directoryIds.add(id);
417             if (getPartitionByDirectoryId(id) == -1) {
418                 DirectoryPartition partition = new DirectoryPartition(false, true);
419                 partition.setDirectoryId(id);
420                 if (DirectoryCompat.isRemoteDirectoryId(id)) {
421                     if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
422                         partition.setLabel(mContext.getString(R.string.directory_search_label_work));
423                     } else {
424                         partition.setLabel(mContext.getString(R.string.directory_search_label));
425                     }
426                 } else {
427                     if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
428                         partition.setLabel(mContext.getString(R.string.list_filter_phones_work));
429                     } else {
430                         partition.setLabel(mDefaultFilterHeaderText.toString());
431                     }
432                 }
433                 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
434                 partition.setDisplayName(cursor.getString(displayNameColumnIndex));
435                 int photoSupport = cursor.getInt(photoSupportColumnIndex);
436                 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
437                         || photoSupport == Directory.PHOTO_SUPPORT_FULL);
438                 addPartition(partition);
439             }
440         }
441 
442         // Phase II: remove deleted directories
443         int count = getPartitionCount();
444         for (int i = count; --i >= 0; ) {
445             Partition partition = getPartition(i);
446             if (partition instanceof DirectoryPartition) {
447                 long id = ((DirectoryPartition)partition).getDirectoryId();
448                 if (!directoryIds.contains(id)) {
449                     removePartition(i);
450                 }
451             }
452         }
453 
454         invalidate();
455         notifyDataSetChanged();
456     }
457 
458     @Override
changeCursor(int partitionIndex, Cursor cursor)459     public void changeCursor(int partitionIndex, Cursor cursor) {
460         if (partitionIndex >= getPartitionCount()) {
461             // There is no partition for this data
462             return;
463         }
464 
465         Partition partition = getPartition(partitionIndex);
466         if (partition instanceof DirectoryPartition) {
467             ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED);
468         }
469 
470         if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
471             mPhotoLoader.refreshCache();
472         }
473 
474         super.changeCursor(partitionIndex, cursor);
475 
476         if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
477             updateIndexer(cursor);
478         }
479 
480         // When the cursor changes, cancel any pending asynchronous photo loads.
481         mPhotoLoader.cancelPendingRequests(mFragmentRootView);
482     }
483 
changeCursor(Cursor cursor)484     public void changeCursor(Cursor cursor) {
485         changeCursor(0, cursor);
486     }
487 
488     /**
489      * Updates the indexer, which is used to produce section headers.
490      */
updateIndexer(Cursor cursor)491     private void updateIndexer(Cursor cursor) {
492         if (cursor == null || cursor.isClosed()) {
493             setIndexer(null);
494             return;
495         }
496 
497         Bundle bundle = cursor.getExtras();
498         if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) &&
499                 bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) {
500             String sections[] =
501                     bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
502             int counts[] = bundle.getIntArray(
503                     Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
504 
505             if (getExtraStartingSection()) {
506                 // Insert an additional unnamed section at the top of the list.
507                 String allSections[] = new String[sections.length + 1];
508                 int allCounts[] = new int[counts.length + 1];
509                 for (int i = 0; i < sections.length; i++) {
510                     allSections[i + 1] = sections[i];
511                     allCounts[i + 1] = counts[i];
512                 }
513                 allCounts[0] = 1;
514                 allSections[0] = "";
515                 setIndexer(new ContactsSectionIndexer(allSections, allCounts));
516             } else {
517                 setIndexer(new ContactsSectionIndexer(sections, counts));
518             }
519         } else {
520             setIndexer(null);
521         }
522     }
523 
getExtraStartingSection()524     protected boolean getExtraStartingSection() {
525         return false;
526     }
527 
528     @Override
getViewTypeCount()529     public int getViewTypeCount() {
530         // We need a separate view type for each item type, plus another one for
531         // each type with header, plus one for "other".
532         return getItemViewTypeCount() * 2 + 1;
533     }
534 
535     @Override
getItemViewType(int partitionIndex, int position)536     public int getItemViewType(int partitionIndex, int position) {
537         int type = super.getItemViewType(partitionIndex, position);
538         if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
539             Placement placement = getItemPlacementInSection(position);
540             return placement.firstInSection ? type : getItemViewTypeCount() + type;
541         } else {
542             return type;
543         }
544     }
545 
546     @Override
isEmpty()547     public boolean isEmpty() {
548         // TODO
549 //        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
550 //            return true;
551 //        }
552 
553         if (!mEmptyListEnabled) {
554             return false;
555         } else if (isSearchMode()) {
556             return TextUtils.isEmpty(getQueryString());
557         } else {
558             return super.isEmpty();
559         }
560     }
561 
isLoading()562     public boolean isLoading() {
563         int count = getPartitionCount();
564         for (int i = 0; i < count; i++) {
565             Partition partition = getPartition(i);
566             if (partition instanceof DirectoryPartition
567                     && ((DirectoryPartition) partition).isLoading()) {
568                 return true;
569             }
570         }
571         return false;
572     }
573 
areAllPartitionsEmpty()574     public boolean areAllPartitionsEmpty() {
575         int count = getPartitionCount();
576         for (int i = 0; i < count; i++) {
577             if (!isPartitionEmpty(i)) {
578                 return false;
579             }
580         }
581         return true;
582     }
583 
584     /**
585      * Changes visibility parameters for the default directory partition.
586      */
configureDefaultPartition(boolean showIfEmpty, boolean hasHeader)587     public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
588         int defaultPartitionIndex = -1;
589         int count = getPartitionCount();
590         for (int i = 0; i < count; i++) {
591             Partition partition = getPartition(i);
592             if (partition instanceof DirectoryPartition &&
593                     ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
594                 defaultPartitionIndex = i;
595                 break;
596             }
597         }
598         if (defaultPartitionIndex != -1) {
599             setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
600             setHasHeader(defaultPartitionIndex, hasHeader);
601         }
602     }
603 
604     @Override
newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent)605     protected View newHeaderView(Context context, int partition, Cursor cursor,
606             ViewGroup parent) {
607         LayoutInflater inflater = LayoutInflater.from(context);
608         View view = inflater.inflate(R.layout.directory_header, parent, false);
609         if (!getPinnedPartitionHeadersEnabled()) {
610             // If the headers are unpinned, there is no need for their background
611             // color to be non-transparent. Setting this transparent reduces maintenance for
612             // non-pinned headers. We don't need to bother synchronizing the activity's
613             // background color with the header background color.
614             view.setBackground(null);
615         }
616         return view;
617     }
618 
bindWorkProfileIcon(final ContactListItemView view, int partitionId)619     protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) {
620         final Partition partition = getPartition(partitionId);
621         if (partition instanceof DirectoryPartition) {
622             final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
623             final long directoryId = directoryPartition.getDirectoryId();
624             final long userType = ContactsUtils.determineUserType(directoryId, null);
625             view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK);
626         }
627     }
628 
629     @Override
bindHeaderView(View view, int partitionIndex, Cursor cursor)630     protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
631         Partition partition = getPartition(partitionIndex);
632         if (!(partition instanceof DirectoryPartition)) {
633             return;
634         }
635 
636         DirectoryPartition directoryPartition = (DirectoryPartition)partition;
637         long directoryId = directoryPartition.getDirectoryId();
638         TextView labelTextView = (TextView)view.findViewById(R.id.label);
639         TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
640         labelTextView.setText(directoryPartition.getLabel());
641         if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) {
642             displayNameTextView.setText(null);
643         } else {
644             String directoryName = directoryPartition.getDisplayName();
645             String displayName = !TextUtils.isEmpty(directoryName)
646                     ? directoryName
647                     : directoryPartition.getDirectoryType();
648             displayNameTextView.setText(displayName);
649         }
650 
651         final Resources res = getContext().getResources();
652         final int headerPaddingTop = partitionIndex == 1 && getPartition(0).isEmpty()?
653                 0 : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding);
654         // There should be no extra padding at the top of the first directory header
655         view.setPaddingRelative(view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(),
656                 view.getPaddingBottom());
657     }
658 
659     // Default implementation simply returns number of rows in the cursor.
660     // Broken out into its own routine so can be overridden by child classes
661     // for eg number of unique contacts for a phone list.
getResultCount(Cursor cursor)662     protected int getResultCount(Cursor cursor) {
663         return cursor == null ? 0 : cursor.getCount();
664     }
665 
666     // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
getQuantityText(int count, int zeroResourceId, int pluralResourceId)667     public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
668         if (count == 0) {
669             return getContext().getString(zeroResourceId);
670         } else {
671             String format = getContext().getResources()
672                     .getQuantityText(pluralResourceId, count).toString();
673             return String.format(format, count);
674         }
675     }
676 
isPhotoSupported(int partitionIndex)677     public boolean isPhotoSupported(int partitionIndex) {
678         Partition partition = getPartition(partitionIndex);
679         if (partition instanceof DirectoryPartition) {
680             return ((DirectoryPartition) partition).isPhotoSupported();
681         }
682         return true;
683     }
684 
685     /**
686      * Returns the currently selected filter.
687      */
getFilter()688     public ContactListFilter getFilter() {
689         return mFilter;
690     }
691 
setFilter(ContactListFilter filter)692     public void setFilter(ContactListFilter filter) {
693         mFilter = filter;
694     }
695 
696     // TODO: move sharable logic (bindXX() methods) to here with extra arguments
697 
698     /**
699      * Loads the photo for the quick contact view and assigns the contact uri.
700      * @param photoIdColumn Index of the photo id column
701      * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
702      * @param contactIdColumn Index of the contact id column
703      * @param lookUpKeyColumn Index of the lookup key column
704      * @param displayNameColumn Index of the display name column
705      */
bindQuickContact(final ContactListItemView view, int partitionIndex, Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, int lookUpKeyColumn, int displayNameColumn)706     protected void bindQuickContact(final ContactListItemView view, int partitionIndex,
707             Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn,
708             int lookUpKeyColumn, int displayNameColumn) {
709         long photoId = 0;
710         if (!cursor.isNull(photoIdColumn)) {
711             photoId = cursor.getLong(photoIdColumn);
712         }
713 
714         QuickContactBadge quickContact = view.getQuickContact();
715         quickContact.assignContactUri(
716                 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
717         if (CompatUtils.hasPrioritizedMimeType()) {
718             // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume
719             // that only Dialer will use this QuickContact badge. This means prioritizing the phone
720             // mimetype here is reasonable.
721             quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
722         }
723 
724         if (photoId != 0 || photoUriColumn == -1) {
725             getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos,
726                     null);
727         } else {
728             final String photoUriString = cursor.getString(photoUriColumn);
729             final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
730             DefaultImageRequest request = null;
731             if (photoUri == null) {
732                 request = getDefaultImageRequestFromCursor(cursor, displayNameColumn,
733                         lookUpKeyColumn);
734             }
735             getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos,
736                     request);
737         }
738 
739     }
740 
741     @Override
hasStableIds()742     public boolean hasStableIds() {
743         // Whenever bindViewId() is called, the values passed into setId() are stable or
744         // stable-ish. For example, when one contact is modified we don't expect a second
745         // contact's Contact._ID values to change.
746         return true;
747     }
748 
bindViewId(final ContactListItemView view, Cursor cursor, int idColumn)749     protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) {
750         // Set a semi-stable id, so that talkback won't get confused when the list gets
751         // refreshed. There is little harm in inserting the same ID twice.
752         long contactId = cursor.getLong(idColumn);
753         view.setId((int) (contactId % Integer.MAX_VALUE));
754 
755     }
756 
getContactUri(int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn)757     protected Uri getContactUri(int partitionIndex, Cursor cursor,
758             int contactIdColumn, int lookUpKeyColumn) {
759         long contactId = cursor.getLong(contactIdColumn);
760         String lookupKey = cursor.getString(lookUpKeyColumn);
761         long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
762         Uri uri = Contacts.getLookupUri(contactId, lookupKey);
763         if (uri != null && directoryId != Directory.DEFAULT) {
764             uri = uri.buildUpon().appendQueryParameter(
765                     ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
766         }
767         return uri;
768     }
769 
770     /**
771      * Retrieves the lookup key and display name from a cursor, and returns a
772      * {@link DefaultImageRequest} containing these contact details
773      *
774      * @param cursor Contacts cursor positioned at the current row to retrieve contact details for
775      * @param displayNameColumn Column index of the display name
776      * @param lookupKeyColumn Column index of the lookup key
777      * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the
778      * display name and lookup key of the contact.
779      */
getDefaultImageRequestFromCursor(Cursor cursor, int displayNameColumn, int lookupKeyColumn)780     public DefaultImageRequest getDefaultImageRequestFromCursor(Cursor cursor,
781             int displayNameColumn, int lookupKeyColumn) {
782         final String displayName = cursor.getString(displayNameColumn);
783         final String lookupKey = cursor.getString(lookupKeyColumn);
784         return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
785     }
786 }
787