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