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 {
onVideoCallIconClicked(int position)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 
PhoneNumberListAdapter(Context context)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 
getUnknownNameText()190     protected CharSequence getUnknownNameText() {
191         return mUnknownNameText;
192     }
193 
194     @Override
configureLoader(CursorLoader loader, long directoryId)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 
isExtendedDirectory(long directoryId)274     protected boolean isExtendedDirectory(long directoryId) {
275         return directoryId >= mFirstExtendedDirectoryId;
276     }
277 
getExtendedDirectoryFromId(long directoryId)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      */
applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId, ContactListFilter filter)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
getContactDisplayName(int position)323     public String getContactDisplayName(int position) {
324         return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
325     }
326 
getPhoneNumber(int position)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      */
getDataUri(int position)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 
getDataUri(int partitionIndex, Cursor cursor)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      */
getLookupKey(int position)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
newView( Context context, int partition, Cursor cursor, int position, ViewGroup parent)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 
setHighlight(ContactListItemView view, Cursor cursor)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
getResultCount(Cursor cursor)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
bindView(View itemView, int partition, Cursor cursor, int position)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 
bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber, int position)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 
bindSectionHeaderAndDivider(final ContactListItemView view, int position)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 
bindName(final ContactListItemView view, Cursor cursor)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 
unbindName(final ContactListItemView view)517     protected void unbindName(final ContactListItemView view) {
518         view.hideDisplayName();
519     }
520 
521     @Override
bindWorkProfileIcon(final ContactListItemView view, int partition)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 
bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor)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 
setPhotoPosition(ContactListItemView.PhotoPosition photoPosition)562     public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
563         mPhotoPosition = photoPosition;
564     }
565 
getPhotoPosition()566     public ContactListItemView.PhotoPosition getPhotoPosition() {
567         return mPhotoPosition;
568     }
569 
setUseCallableUri(boolean useCallableUri)570     public void setUseCallableUri(boolean useCallableUri) {
571         mUseCallableUri = useCallableUri;
572     }
573 
usesCallableUri()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
changeDirectories(Cursor cursor)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
getContactUri(int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn)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 
getListener()650     public Listener getListener() {
651         return mListener;
652     }
653 
setListener(Listener listener)654     public void setListener(Listener listener) {
655         mListener = listener;
656     }
657 }
658