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