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.ContentUris;
     19 import android.content.Context;
     20 import android.content.CursorLoader;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.net.Uri.Builder;
     24 import android.provider.ContactsContract;
     25 import android.provider.ContactsContract.CommonDataKinds.Callable;
     26 import android.provider.ContactsContract.CommonDataKinds.Phone;
     27 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     28 import android.provider.ContactsContract.Contacts;
     29 import android.provider.ContactsContract.Data;
     30 import android.provider.ContactsContract.Directory;
     31 import android.text.TextUtils;
     32 import android.util.Log;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 
     36 import com.android.contacts.common.CallUtil;
     37 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
     38 import com.android.contacts.common.ContactsUtils;
     39 import com.android.contacts.common.GeoUtil;
     40 import com.android.contacts.common.R;
     41 import com.android.contacts.common.compat.CallableCompat;
     42 import com.android.contacts.common.compat.CompatUtils;
     43 import com.android.contacts.common.compat.DirectoryCompat;
     44 import com.android.contacts.common.compat.PhoneCompat;
     45 import com.android.contacts.common.extensions.ExtendedPhoneDirectoriesManager;
     46 import com.android.contacts.common.extensions.ExtensionsFactory;
     47 import com.android.contacts.common.preference.ContactsPreferences;
     48 import com.android.contacts.common.util.Constants;
     49 
     50 import com.google.common.collect.Lists;
     51 
     52 import java.util.ArrayList;
     53 import java.util.List;
     54 
     55 /**
     56  * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and
     57  * {@link SipAddress#CONTENT_ITEM_TYPE}.
     58  *
     59  * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is
     60  * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
     61  * API instead of {@link Phone}.
     62  */
     63 public class PhoneNumberListAdapter extends ContactEntryListAdapter {
     64 
     65     private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
     66 
     67     public interface Listener {
     68         void onVideoCallIconClicked(int position);
     69     }
     70 
     71     // A list of extended directories to add to the directories from the database
     72     private final List<DirectoryPartition> mExtendedDirectories;
     73 
     74     // Extended directories will have ID's that are higher than any of the id's from the database,
     75     // so that we can identify them and set them up properly. If no extended directories
     76     // exist, this will be Long.MAX_VALUE
     77     private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
     78 
     79     public static class PhoneQuery {
     80 
     81         /**
     82          * Optional key used as part of a JSON lookup key to specify an analytics category
     83          * associated with the row.
     84          */
     85         public static final String ANALYTICS_CATEGORY = "analytics_category";
     86 
     87         /**
     88          * Optional key used as part of a JSON lookup key to specify an analytics action associated
     89          * with the row.
     90          */
     91         public static final String ANALYTICS_ACTION = "analytics_action";
     92 
     93         /**
     94          * Optional key used as part of a JSON lookup key to specify an analytics value associated
     95          * with the row.
     96          */
     97         public static final String ANALYTICS_VALUE = "analytics_value";
     98 
     99         public static final String[] PROJECTION_PRIMARY_INTERNAL = new String[] {
    100             Phone._ID,                          // 0
    101             Phone.TYPE,                         // 1
    102             Phone.LABEL,                        // 2
    103             Phone.NUMBER,                       // 3
    104             Phone.CONTACT_ID,                   // 4
    105             Phone.LOOKUP_KEY,                   // 5
    106             Phone.PHOTO_ID,                     // 6
    107             Phone.DISPLAY_NAME_PRIMARY,         // 7
    108             Phone.PHOTO_THUMBNAIL_URI,          // 8
    109         };
    110 
    111         public static final String[] PROJECTION_PRIMARY;
    112 
    113         static {
    114             final List<String> projectionList = Lists.newArrayList(PROJECTION_PRIMARY_INTERNAL);
    115             if (CompatUtils.isMarshmallowCompatible()) {
    116                 projectionList.add(Phone.CARRIER_PRESENCE); // 9
    117             }
    118             PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]);
    119         }
    120 
    121         public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = new String[] {
    122             Phone._ID,                          // 0
    123             Phone.TYPE,                         // 1
    124             Phone.LABEL,                        // 2
    125             Phone.NUMBER,                       // 3
    126             Phone.CONTACT_ID,                   // 4
    127             Phone.LOOKUP_KEY,                   // 5
    128             Phone.PHOTO_ID,                     // 6
    129             Phone.DISPLAY_NAME_ALTERNATIVE,     // 7
    130             Phone.PHOTO_THUMBNAIL_URI,          // 8
    131         };
    132 
    133         public static final String[] PROJECTION_ALTERNATIVE;
    134 
    135         static {
    136             final List<String> projectionList = Lists.newArrayList(PROJECTION_ALTERNATIVE_INTERNAL);
    137             if (CompatUtils.isMarshmallowCompatible()) {
    138                 projectionList.add(Phone.CARRIER_PRESENCE); // 9
    139             }
    140             PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]);
    141         }
    142 
    143         public static final int PHONE_ID                = 0;
    144         public static final int PHONE_TYPE              = 1;
    145         public static final int PHONE_LABEL             = 2;
    146         public static final int PHONE_NUMBER            = 3;
    147         public static final int CONTACT_ID              = 4;
    148         public static final int LOOKUP_KEY              = 5;
    149         public static final int PHOTO_ID                = 6;
    150         public static final int DISPLAY_NAME            = 7;
    151         public static final int PHOTO_URI               = 8;
    152         public static final int CARRIER_PRESENCE        = 9;
    153     }
    154 
    155     private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE =
    156             "length(" + Phone.NUMBER + ") < 1000";
    157 
    158     private final CharSequence mUnknownNameText;
    159     private final String mCountryIso;
    160 
    161     private ContactListItemView.PhotoPosition mPhotoPosition;
    162 
    163     private boolean mUseCallableUri;
    164 
    165     private Listener mListener;
    166 
    167     private boolean mIsVideoEnabled;
    168     private boolean mIsPresenceEnabled;
    169 
    170     public PhoneNumberListAdapter(Context context) {
    171         super(context);
    172         setDefaultFilterHeaderText(R.string.list_filter_phones);
    173         mUnknownNameText = context.getText(android.R.string.unknownName);
    174         mCountryIso = GeoUtil.getCurrentCountryIso(context);
    175 
    176         final ExtendedPhoneDirectoriesManager manager
    177                 = ExtensionsFactory.getExtendedPhoneDirectoriesManager();
    178         if (manager != null) {
    179             mExtendedDirectories = manager.getExtendedDirectories(mContext);
    180         } else {
    181             // Empty list to avoid sticky NPE's
    182             mExtendedDirectories = new ArrayList<DirectoryPartition>();
    183         }
    184 
    185         int videoCapabilities = CallUtil.getVideoCallingAvailability(context);
    186         mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0;
    187         mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
    188     }
    189 
    190     protected CharSequence getUnknownNameText() {
    191         return mUnknownNameText;
    192     }
    193 
    194     @Override
    195     public void configureLoader(CursorLoader loader, long directoryId) {
    196         String query = getQueryString();
    197         if (query == null) {
    198             query = "";
    199         }
    200         if (isExtendedDirectory(directoryId)) {
    201             final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
    202             final String contentUri = directory.getContentUri();
    203             if (contentUri == null) {
    204                 throw new IllegalStateException("Extended directory must have a content URL: "
    205                         + directory);
    206             }
    207             final Builder builder = Uri.parse(contentUri).buildUpon();
    208             builder.appendPath(query);
    209             builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
    210                     String.valueOf(getDirectoryResultLimit(directory)));
    211             loader.setUri(builder.build());
    212             loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
    213         } else {
    214             final boolean isRemoteDirectoryQuery
    215                     = DirectoryCompat.isRemoteDirectoryId(directoryId);
    216             final Builder builder;
    217             if (isSearchMode()) {
    218                 final Uri baseUri;
    219                 if (isRemoteDirectoryQuery) {
    220                     baseUri = PhoneCompat.getContentFilterUri();
    221                 } else if (mUseCallableUri) {
    222                     baseUri = CallableCompat.getContentFilterUri();
    223                 } else {
    224                     baseUri = PhoneCompat.getContentFilterUri();
    225                 }
    226                 builder = baseUri.buildUpon();
    227                 builder.appendPath(query);      // Builder will encode the query
    228                 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
    229                         String.valueOf(directoryId));
    230                 if (isRemoteDirectoryQuery) {
    231                     builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
    232                             String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
    233                 }
    234             } else {
    235                 Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
    236                 builder = baseUri.buildUpon().appendQueryParameter(
    237                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
    238                 if (isSectionHeaderDisplayEnabled()) {
    239                     builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true");
    240                 }
    241                 applyFilter(loader, builder, directoryId, getFilter());
    242             }
    243 
    244             // Ignore invalid phone numbers that are too long. These can potentially cause freezes
    245             // in the UI and there is no reason to display them.
    246             final String prevSelection = loader.getSelection();
    247             final String newSelection;
    248             if (!TextUtils.isEmpty(prevSelection)) {
    249                 newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE;
    250             } else {
    251                 newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE;
    252             }
    253             loader.setSelection(newSelection);
    254 
    255             // Remove duplicates when it is possible.
    256             builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
    257             loader.setUri(builder.build());
    258 
    259             // TODO a projection that includes the search snippet
    260             if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
    261                 loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
    262             } else {
    263                 loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
    264             }
    265 
    266             if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
    267                 loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
    268             } else {
    269                 loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
    270             }
    271         }
    272     }
    273 
    274     protected boolean isExtendedDirectory(long directoryId) {
    275         return directoryId >= mFirstExtendedDirectoryId;
    276     }
    277 
    278     private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
    279         final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
    280         return mExtendedDirectories.get(directoryIndex);
    281     }
    282 
    283     /**
    284      * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
    285      * filter}.
    286      */
    287     private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId,
    288             ContactListFilter filter) {
    289         if (filter == null || directoryId != Directory.DEFAULT) {
    290             return;
    291         }
    292 
    293         final StringBuilder selection = new StringBuilder();
    294         final List<String> selectionArgs = new ArrayList<String>();
    295 
    296         switch (filter.filterType) {
    297             case ContactListFilter.FILTER_TYPE_CUSTOM: {
    298                 selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
    299                 selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
    300                 break;
    301             }
    302             case ContactListFilter.FILTER_TYPE_ACCOUNT: {
    303                 filter.addAccountQueryParameterToUrl(uriBuilder);
    304                 break;
    305             }
    306             case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
    307             case ContactListFilter.FILTER_TYPE_DEFAULT:
    308                 break; // No selection needed.
    309             case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
    310                 break; // This adapter is always "phone only", so no selection needed either.
    311             default:
    312                 Log.w(TAG, "Unsupported filter type came " +
    313                         "(type: " + filter.filterType + ", toString: " + filter + ")" +
    314                         " showing all contacts.");
    315                 // No selection.
    316                 break;
    317         }
    318         loader.setSelection(selection.toString());
    319         loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
    320     }
    321 
    322     @Override
    323     public String getContactDisplayName(int position) {
    324         return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
    325     }
    326 
    327     public String getPhoneNumber(int position) {
    328         final Cursor item = (Cursor)getItem(position);
    329         return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
    330     }
    331 
    332     /**
    333      * Builds a {@link Data#CONTENT_URI} for the given cursor position.
    334      *
    335      * @return Uri for the data. may be null if the cursor is not ready.
    336      */
    337     public Uri getDataUri(int position) {
    338         final int partitionIndex = getPartitionForPosition(position);
    339         final Cursor item = (Cursor)getItem(position);
    340         return item != null ? getDataUri(partitionIndex, item) : null;
    341     }
    342 
    343     public Uri getDataUri(int partitionIndex, Cursor cursor) {
    344         final long directoryId =
    345                 ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
    346         if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
    347             return null;
    348         } else if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
    349             /*
    350              * ContentUris.withAppendedId(Data.CONTENT_URI, phoneId), is invalid if
    351              * isEnterpriseDirectoryId returns true, because the uri itself will fail since the
    352              * ContactsProvider in Android Framework currently doesn't support it. return null until
    353              * Android framework has enterprise version of Data.CONTENT_URI
    354              */
    355             return null;
    356         } else {
    357             final long phoneId = cursor.getLong(PhoneQuery.PHONE_ID);
    358             return ContentUris.withAppendedId(Data.CONTENT_URI, phoneId);
    359         }
    360     }
    361 
    362     /**
    363      * Retrieves the lookup key for the given cursor position.
    364      *
    365      * @param position The cursor position.
    366      * @return The lookup key.
    367      */
    368     public String getLookupKey(int position) {
    369         final Cursor item = (Cursor)getItem(position);
    370         return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null;
    371     }
    372 
    373     @Override
    374     protected ContactListItemView newView(
    375             Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
    376         ContactListItemView view = super.newView(context, partition, cursor, position, parent);
    377         view.setUnknownNameText(mUnknownNameText);
    378         view.setQuickContactEnabled(isQuickContactEnabled());
    379         view.setPhotoPosition(mPhotoPosition);
    380         return view;
    381     }
    382 
    383     protected void setHighlight(ContactListItemView view, Cursor cursor) {
    384         view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
    385     }
    386 
    387     // Override default, which would return number of phone numbers, so we
    388     // instead return number of contacts.
    389     @Override
    390     protected int getResultCount(Cursor cursor) {
    391         if (cursor == null) {
    392             return 0;
    393         }
    394         cursor.moveToPosition(-1);
    395         long curContactId = -1;
    396         int numContacts = 0;
    397         while(cursor.moveToNext()) {
    398             final long contactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    399             if (contactId != curContactId) {
    400                 curContactId = contactId;
    401                 ++numContacts;
    402             }
    403         }
    404         return numContacts;
    405     }
    406 
    407     @Override
    408     protected void bindView(View itemView, int partition, Cursor cursor, int position) {
    409         super.bindView(itemView, partition, cursor, position);
    410         ContactListItemView view = (ContactListItemView)itemView;
    411 
    412         setHighlight(view, cursor);
    413 
    414         // Look at elements before and after this position, checking if contact IDs are same.
    415         // If they have one same contact ID, it means they can be grouped.
    416         //
    417         // In one group, only the first entry will show its photo and its name, and the other
    418         // entries in the group show just their data (e.g. phone number, email address).
    419         cursor.moveToPosition(position);
    420         boolean isFirstEntry = true;
    421         boolean showBottomDivider = true;
    422         final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    423         if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
    424             final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    425             if (currentContactId == previousContactId) {
    426                 isFirstEntry = false;
    427             }
    428         }
    429         cursor.moveToPosition(position);
    430         if (cursor.moveToNext() && !cursor.isAfterLast()) {
    431             final long nextContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    432             if (currentContactId == nextContactId) {
    433                 // The following entry should be in the same group, which means we don't want a
    434                 // divider between them.
    435                 // TODO: we want a different divider than the divider between groups. Just hiding
    436                 // this divider won't be enough.
    437                 showBottomDivider = false;
    438             }
    439         }
    440         cursor.moveToPosition(position);
    441 
    442         bindViewId(view, cursor, PhoneQuery.PHONE_ID);
    443 
    444         bindSectionHeaderAndDivider(view, position);
    445         if (isFirstEntry) {
    446             bindName(view, cursor);
    447             if (isQuickContactEnabled()) {
    448                 bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
    449                         PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID,
    450                         PhoneQuery.LOOKUP_KEY, PhoneQuery.DISPLAY_NAME);
    451             } else {
    452                 if (getDisplayPhotos()) {
    453                     bindPhoto(view, partition, cursor);
    454                 }
    455             }
    456         } else {
    457             unbindName(view);
    458 
    459             view.removePhotoView(true, false);
    460         }
    461 
    462         final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
    463         bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position);
    464     }
    465 
    466     protected void bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber,
    467             int position) {
    468         CharSequence label = null;
    469         if (displayNumber &&  !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
    470             final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
    471             final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
    472 
    473             // TODO cache
    474             label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
    475         }
    476         view.setLabel(label);
    477         final String text;
    478         if (displayNumber) {
    479             text = cursor.getString(PhoneQuery.PHONE_NUMBER);
    480         } else {
    481             // Display phone label. If that's null, display geocoded location for the number
    482             final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
    483             if (phoneLabel != null) {
    484                 text = phoneLabel;
    485             } else {
    486                 final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
    487                 text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
    488             }
    489         }
    490         view.setPhoneNumber(text, mCountryIso);
    491 
    492         if (CompatUtils.isVideoCompatible()) {
    493             // Determine if carrier presence indicates the number supports video calling.
    494             int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE);
    495             boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
    496 
    497             boolean isVideoIconShown = mIsVideoEnabled && (
    498                     mIsPresenceEnabled && isPresent || !mIsPresenceEnabled);
    499             view.setShowVideoCallIcon(isVideoIconShown, mListener, position);
    500         }
    501     }
    502 
    503     protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
    504         if (isSectionHeaderDisplayEnabled()) {
    505             Placement placement = getItemPlacementInSection(position);
    506             view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
    507         } else {
    508             view.setSectionHeader(null);
    509         }
    510     }
    511 
    512     protected void bindName(final ContactListItemView view, Cursor cursor) {
    513         view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
    514         // Note: we don't show phonetic names any more (see issue 5265330)
    515     }
    516 
    517     protected void unbindName(final ContactListItemView view) {
    518         view.hideDisplayName();
    519     }
    520 
    521     @Override
    522     protected void bindWorkProfileIcon(final ContactListItemView view, int partition) {
    523         final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
    524         final long directoryId = directory.getDirectoryId();
    525         final long userType = ContactsUtils.determineUserType(directoryId, null);
    526         // Work directory must not be a extended directory. An extended directory is custom
    527         // directory in the app, but not a directory provided by framework. So it can't be
    528         // USER_TYPE_WORK.
    529         view.setWorkProfileIconEnabled(
    530                 !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK);
    531     }
    532 
    533     protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
    534         if (!isPhotoSupported(partitionIndex)) {
    535             view.removePhotoView();
    536             return;
    537         }
    538 
    539         long photoId = 0;
    540         if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
    541             photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
    542         }
    543 
    544         if (photoId != 0) {
    545             getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false,
    546                     getCircularPhotos(), null);
    547         } else {
    548             final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
    549             final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
    550 
    551             DefaultImageRequest request = null;
    552             if (photoUri == null) {
    553                 final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME);
    554                 final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY);
    555                 request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos());
    556             }
    557             getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
    558                     getCircularPhotos(), request);
    559         }
    560     }
    561 
    562     public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
    563         mPhotoPosition = photoPosition;
    564     }
    565 
    566     public ContactListItemView.PhotoPosition getPhotoPosition() {
    567         return mPhotoPosition;
    568     }
    569 
    570     public void setUseCallableUri(boolean useCallableUri) {
    571         mUseCallableUri = useCallableUri;
    572     }
    573 
    574     public boolean usesCallableUri() {
    575         return mUseCallableUri;
    576     }
    577 
    578     /**
    579      * Override base implementation to inject extended directories between local & remote
    580      * directories. This is done in the following steps:
    581      * 1. Call base implementation to add directories from the cursor.
    582      * 2. Iterate all base directories and establish the following information:
    583      *   a. The highest directory id so that we can assign unused id's to the extended directories.
    584      *   b. The index of the last non-remote directory. This is where we will insert extended
    585      *      directories.
    586      * 3. Iterate the extended directories and for each one, assign an ID and insert it in the
    587      *    proper location.
    588      */
    589     @Override
    590     public void changeDirectories(Cursor cursor) {
    591         super.changeDirectories(cursor);
    592         if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
    593             return;
    594         }
    595         final int numExtendedDirectories = mExtendedDirectories.size();
    596         if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
    597             // already added all directories;
    598             return;
    599         }
    600         //
    601         mFirstExtendedDirectoryId = Long.MAX_VALUE;
    602         if (numExtendedDirectories > 0) {
    603             // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
    604             // "special" ID.
    605             long maxId = Directory.LOCAL_INVISIBLE;
    606             int insertIndex = 0;
    607             for (int i = 0, n = getPartitionCount(); i < n; i++) {
    608                 final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
    609                 final long id = partition.getDirectoryId();
    610                 if (id > maxId) {
    611                     maxId = id;
    612                 }
    613                 if (!DirectoryCompat.isRemoteDirectoryId(id)) {
    614                     // assuming remote directories come after local, we will end up with the index
    615                     // where we should insert extended directories. This also works if there are no
    616                     // remote directories at all.
    617                     insertIndex = i + 1;
    618                 }
    619             }
    620             // Extended directories ID's cannot collide with base directories
    621             mFirstExtendedDirectoryId = maxId + 1;
    622             for (int i = 0; i < numExtendedDirectories; i++) {
    623                 final long id = mFirstExtendedDirectoryId + i;
    624                 final DirectoryPartition directory = mExtendedDirectories.get(i);
    625                 if (getPartitionByDirectoryId(id) == -1) {
    626                     addPartition(insertIndex, directory);
    627                     directory.setDirectoryId(id);
    628                 }
    629             }
    630         }
    631     }
    632 
    633     @Override
    634     protected Uri getContactUri(int partitionIndex, Cursor cursor,
    635             int contactIdColumn, int lookUpKeyColumn) {
    636         final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex);
    637         final long directoryId = directory.getDirectoryId();
    638         if (!isExtendedDirectory(directoryId)) {
    639             return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn);
    640         }
    641         return Contacts.CONTENT_LOOKUP_URI.buildUpon()
    642                 .appendPath(Constants.LOOKUP_URI_ENCODED)
    643                 .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel())
    644                 .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
    645                         String.valueOf(directoryId))
    646                 .encodedFragment(cursor.getString(lookUpKeyColumn))
    647                 .build();
    648     }
    649 
    650     public Listener getListener() {
    651         return mListener;
    652     }
    653 
    654     public void setListener(Listener listener) {
    655         mListener = listener;
    656     }
    657 }
    658