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