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