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.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 
     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      */
    109     protected void setFragmentRootView(View fragmentRootView) {
    110         mFragmentRootView = fragmentRootView;
    111     }
    112 
    113     protected void setDefaultFilterHeaderText(int resourceId) {
    114         mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
    115     }
    116 
    117     @Override
    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
    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
    134     protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
    135         return new ContactListPinnedHeaderView(context, null, parent);
    136     }
    137 
    138     @Override
    139     protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
    140         ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
    141     }
    142 
    143     protected void addPartitions() {
    144         addPartition(createDefaultDirectoryPartition());
    145     }
    146 
    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      */
    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 
    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 
    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 
    206     public abstract String getContactDisplayName(int position);
    207     public abstract void configureLoader(CursorLoader loader, long directoryId);
    208 
    209     /**
    210      * Marks all partitions as "loading"
    211      */
    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
    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 
    243     public boolean isSearchMode() {
    244         return mSearchMode;
    245     }
    246 
    247     public void setSearchMode(boolean flag) {
    248         mSearchMode = flag;
    249     }
    250 
    251     public String getQueryString() {
    252         return mQueryString;
    253     }
    254 
    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 
    265     public String getUpperCaseQueryString() {
    266         return mUpperCaseQueryString;
    267     }
    268 
    269     public int getDirectorySearchMode() {
    270         return mDirectorySearchMode;
    271     }
    272 
    273     public void setDirectorySearchMode(int mode) {
    274         mDirectorySearchMode = mode;
    275     }
    276 
    277     public int getDirectoryResultLimit() {
    278         return mDirectoryResultLimit;
    279     }
    280 
    281     public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
    282         final int limit = directoryPartition.getResultLimit();
    283         return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
    284     }
    285 
    286     public void setDirectoryResultLimit(int limit) {
    287         this.mDirectoryResultLimit = limit;
    288     }
    289 
    290     public int getContactNameDisplayOrder() {
    291         return mDisplayOrder;
    292     }
    293 
    294     public void setContactNameDisplayOrder(int displayOrder) {
    295         mDisplayOrder = displayOrder;
    296     }
    297 
    298     public int getSortOrder() {
    299         return mSortOrder;
    300     }
    301 
    302     public void setSortOrder(int sortOrder) {
    303         mSortOrder = sortOrder;
    304     }
    305 
    306     public void setPhotoLoader(ContactPhotoManager photoLoader) {
    307         mPhotoLoader = photoLoader;
    308     }
    309 
    310     protected ContactPhotoManager getPhotoLoader() {
    311         return mPhotoLoader;
    312     }
    313 
    314     public boolean getDisplayPhotos() {
    315         return mDisplayPhotos;
    316     }
    317 
    318     public void setDisplayPhotos(boolean displayPhotos) {
    319         mDisplayPhotos = displayPhotos;
    320     }
    321 
    322     public boolean getCircularPhotos() {
    323         return mCircularPhotos;
    324     }
    325 
    326     public void setCircularPhotos(boolean circularPhotos) {
    327         mCircularPhotos = circularPhotos;
    328     }
    329 
    330     public boolean isEmptyListEnabled() {
    331         return mEmptyListEnabled;
    332     }
    333 
    334     public void setEmptyListEnabled(boolean flag) {
    335         mEmptyListEnabled = flag;
    336     }
    337 
    338     public boolean isSelectionVisible() {
    339         return mSelectionVisible;
    340     }
    341 
    342     public void setSelectionVisible(boolean flag) {
    343         this.mSelectionVisible = flag;
    344     }
    345 
    346     public boolean isQuickContactEnabled() {
    347         return mQuickContactEnabled;
    348     }
    349 
    350     public void setQuickContactEnabled(boolean quickContactEnabled) {
    351         mQuickContactEnabled = quickContactEnabled;
    352     }
    353 
    354     public boolean isAdjustSelectionBoundsEnabled() {
    355         return mAdjustSelectionBoundsEnabled;
    356     }
    357 
    358     public void setAdjustSelectionBoundsEnabled(boolean enabled) {
    359         mAdjustSelectionBoundsEnabled = enabled;
    360     }
    361 
    362     public boolean shouldIncludeFavorites() {
    363         return mIncludeFavorites;
    364     }
    365 
    366     public void setIncludeFavorites(boolean includeFavorites) {
    367         mIncludeFavorites = includeFavorites;
    368     }
    369 
    370     public void setFavoritesSectionHeader(int numberOfFavorites) {
    371         if (mIncludeFavorites) {
    372             mNumberOfFavorites = numberOfFavorites;
    373             setSectionHeader(numberOfFavorites);
    374         }
    375     }
    376 
    377     public int getNumberOfFavorites() {
    378         return mNumberOfFavorites;
    379     }
    380 
    381     private void setSectionHeader(int numberOfItems) {
    382         SectionIndexer indexer = getIndexer();
    383         if (indexer != null) {
    384             ((ContactsSectionIndexer) indexer).setFavoritesHeader(numberOfItems);
    385         }
    386     }
    387 
    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      */
    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
    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 
    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      */
    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 
    524     protected boolean getExtraStartingSection() {
    525         return false;
    526     }
    527 
    528     @Override
    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
    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
    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 
    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 
    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      */
    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
    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 
    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
    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.
    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
    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 
    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      */
    688     public ContactListFilter getFilter() {
    689         return mFilter;
    690     }
    691 
    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      */
    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
    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 
    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 
    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      */
    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