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