Home | History | Annotate | Download | only in list
      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 
    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      */
    113     protected void setFragmentRootView(View fragmentRootView) {
    114         mFragmentRootView = fragmentRootView;
    115     }
    116 
    117     protected void setDefaultFilterHeaderText(int resourceId) {
    118         mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
    119     }
    120 
    121     @Override
    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
    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
    138     protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
    139         return new ContactListPinnedHeaderView(context, null, parent);
    140     }
    141 
    142     @Override
    143     protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
    144         ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
    145     }
    146 
    147     protected void addPartitions() {
    148         addPartition(createDefaultDirectoryPartition());
    149     }
    150 
    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      */
    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 
    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 
    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 
    210     public abstract String getContactDisplayName(int position);
    211     public abstract void configureLoader(CursorLoader loader, long directoryId);
    212 
    213     /**
    214      * Marks all partitions as "loading"
    215      */
    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
    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 
    247     public boolean isSearchMode() {
    248         return mSearchMode;
    249     }
    250 
    251     public void setSearchMode(boolean flag) {
    252         mSearchMode = flag;
    253     }
    254 
    255     public String getQueryString() {
    256         return mQueryString;
    257     }
    258 
    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 
    269     public String getUpperCaseQueryString() {
    270         return mUpperCaseQueryString;
    271     }
    272 
    273     public int getDirectorySearchMode() {
    274         return mDirectorySearchMode;
    275     }
    276 
    277     public void setDirectorySearchMode(int mode) {
    278         mDirectorySearchMode = mode;
    279     }
    280 
    281     public int getDirectoryResultLimit() {
    282         return mDirectoryResultLimit;
    283     }
    284 
    285     public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
    286         final int limit = directoryPartition.getResultLimit();
    287         return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
    288     }
    289 
    290     public void setDirectoryResultLimit(int limit) {
    291         this.mDirectoryResultLimit = limit;
    292     }
    293 
    294     public int getContactNameDisplayOrder() {
    295         return mDisplayOrder;
    296     }
    297 
    298     public void setContactNameDisplayOrder(int displayOrder) {
    299         mDisplayOrder = displayOrder;
    300     }
    301 
    302     public int getSortOrder() {
    303         return mSortOrder;
    304     }
    305 
    306     public void setSortOrder(int sortOrder) {
    307         mSortOrder = sortOrder;
    308     }
    309 
    310     public void setPhotoLoader(ContactPhotoManager photoLoader) {
    311         mPhotoLoader = photoLoader;
    312     }
    313 
    314     protected ContactPhotoManager getPhotoLoader() {
    315         return mPhotoLoader;
    316     }
    317 
    318     public boolean getDisplayPhotos() {
    319         return mDisplayPhotos;
    320     }
    321 
    322     public void setDisplayPhotos(boolean displayPhotos) {
    323         mDisplayPhotos = displayPhotos;
    324     }
    325 
    326     public boolean getCircularPhotos() {
    327         return mCircularPhotos;
    328     }
    329 
    330     public void setCircularPhotos(boolean circularPhotos) {
    331         mCircularPhotos = circularPhotos;
    332     }
    333 
    334     public boolean isEmptyListEnabled() {
    335         return mEmptyListEnabled;
    336     }
    337 
    338     public void setEmptyListEnabled(boolean flag) {
    339         mEmptyListEnabled = flag;
    340     }
    341 
    342     public boolean isSelectionVisible() {
    343         return mSelectionVisible;
    344     }
    345 
    346     public void setSelectionVisible(boolean flag) {
    347         this.mSelectionVisible = flag;
    348     }
    349 
    350     public boolean isQuickContactEnabled() {
    351         return mQuickContactEnabled;
    352     }
    353 
    354     public void setQuickContactEnabled(boolean quickContactEnabled) {
    355         mQuickContactEnabled = quickContactEnabled;
    356     }
    357 
    358     public boolean isAdjustSelectionBoundsEnabled() {
    359         return mAdjustSelectionBoundsEnabled;
    360     }
    361 
    362     public void setAdjustSelectionBoundsEnabled(boolean enabled) {
    363         mAdjustSelectionBoundsEnabled = enabled;
    364     }
    365 
    366     public boolean shouldIncludeProfile() {
    367         return mIncludeProfile;
    368     }
    369 
    370     public void setIncludeProfile(boolean includeProfile) {
    371         mIncludeProfile = includeProfile;
    372     }
    373 
    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 
    386     public boolean hasProfile() {
    387         return mProfileExists;
    388     }
    389 
    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      */
    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
    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 
    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      */
    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 
    526     protected boolean getExtraStartingSection() {
    527         return false;
    528     }
    529 
    530     @Override
    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
    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
    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 
    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 
    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      */
    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
    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 
    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
    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.
    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      */
    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
    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 
    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      */
    719     public ContactListFilter getFilter() {
    720         return mFilter;
    721     }
    722 
    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      */
    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
    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 
    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 
    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      */
    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