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