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