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.list; 17 18 import android.content.Context; 19 import android.content.CursorLoader; 20 import android.content.res.Resources; 21 import android.database.Cursor; 22 import android.net.Uri; 23 import android.os.Bundle; 24 import android.provider.ContactsContract; 25 import android.provider.ContactsContract.CommonDataKinds.Phone; 26 import android.provider.ContactsContract.Contacts; 27 import android.provider.ContactsContract.Directory; 28 import android.text.TextUtils; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.QuickContactBadge; 34 import android.widget.SectionIndexer; 35 import android.widget.TextView; 36 37 import com.android.contacts.ContactPhotoManager; 38 import com.android.contacts.ContactPhotoManager.DefaultImageRequest; 39 import com.android.contacts.ContactsUtils; 40 import com.android.contacts.R; 41 import com.android.contacts.compat.CompatUtils; 42 import com.android.contacts.compat.DirectoryCompat; 43 import com.android.contacts.util.SearchUtil; 44 45 import java.util.HashSet; 46 47 /** 48 * Common base class for various contact-related lists, e.g. contact list, phone number list 49 * etc. 50 */ 51 public abstract class ContactEntryListAdapter extends IndexerListAdapter { 52 53 private static final String TAG = "ContactEntryListAdapter"; 54 55 /** 56 * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should 57 * be included in the search. 58 */ 59 public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false; 60 61 private int mDisplayOrder; 62 private int mSortOrder; 63 64 private boolean mDisplayPhotos; 65 private boolean mCircularPhotos = true; 66 private boolean mQuickContactEnabled; 67 private boolean mAdjustSelectionBoundsEnabled; 68 69 /** 70 * indicates if contact queries include favorites 71 */ 72 private boolean mIncludeFavorites; 73 74 private int mNumberOfFavorites; 75 76 /** 77 * The root view of the fragment that this adapter is associated with. 78 */ 79 private View mFragmentRootView; 80 81 private ContactPhotoManager mPhotoLoader; 82 83 private String mQueryString; 84 private String mUpperCaseQueryString; 85 private boolean mSearchMode; 86 private int mDirectorySearchMode; 87 private int mDirectoryResultLimit = Integer.MAX_VALUE; 88 89 private boolean mEmptyListEnabled = true; 90 91 private boolean mSelectionVisible; 92 93 private ContactListFilter mFilter; 94 private boolean mDarkTheme = false; 95 96 /** Resource used to provide header-text for default filter. */ 97 private CharSequence mDefaultFilterHeaderText; 98 99 public ContactEntryListAdapter(Context context) { 100 super(context); 101 setDefaultFilterHeaderText(R.string.local_search_label); 102 addPartitions(); 103 } 104 105 /** 106 * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of 107 * image loading requests that get cancelled on cursor changes. 108 */ 109 protected void setFragmentRootView(View fragmentRootView) { 110 mFragmentRootView = fragmentRootView; 111 } 112 113 protected void setDefaultFilterHeaderText(int resourceId) { 114 mDefaultFilterHeaderText = getContext().getResources().getText(resourceId); 115 } 116 117 @Override 118 protected ContactListItemView newView( 119 Context context, int partition, Cursor cursor, int position, ViewGroup parent) { 120 final ContactListItemView view = new ContactListItemView(context, null); 121 view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); 122 view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); 123 return view; 124 } 125 126 @Override 127 protected void bindView(View itemView, int partition, Cursor cursor, int position) { 128 final ContactListItemView view = (ContactListItemView) itemView; 129 view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); 130 bindWorkProfileIcon(view, partition); 131 } 132 133 @Override 134 protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) { 135 return new ContactListPinnedHeaderView(context, null, parent); 136 } 137 138 @Override 139 protected void setPinnedSectionTitle(View pinnedHeaderView, String title) { 140 ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title); 141 } 142 143 protected void addPartitions() { 144 addPartition(createDefaultDirectoryPartition()); 145 } 146 147 protected DirectoryPartition createDefaultDirectoryPartition() { 148 DirectoryPartition partition = new DirectoryPartition(true, true); 149 partition.setDirectoryId(Directory.DEFAULT); 150 partition.setDirectoryType(getContext().getString(R.string.contactsList)); 151 partition.setPriorityDirectory(true); 152 partition.setPhotoSupported(true); 153 partition.setLabel(mDefaultFilterHeaderText.toString()); 154 return partition; 155 } 156 157 /** 158 * Remove all directories after the default directory. This is typically used when contacts 159 * list screens are asked to exit the search mode and thus need to remove all remote directory 160 * results for the search. 161 * 162 * This code assumes that the default directory and directories before that should not be 163 * deleted (e.g. Join screen has "suggested contacts" directory before the default director, 164 * and we should not remove the directory). 165 */ 166 public void removeDirectoriesAfterDefault() { 167 final int partitionCount = getPartitionCount(); 168 for (int i = partitionCount - 1; i >= 0; i--) { 169 final Partition partition = getPartition(i); 170 if ((partition instanceof DirectoryPartition) 171 && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) { 172 break; 173 } else { 174 removePartition(i); 175 } 176 } 177 } 178 179 protected int getPartitionByDirectoryId(long id) { 180 int count = getPartitionCount(); 181 for (int i = 0; i < count; i++) { 182 Partition partition = getPartition(i); 183 if (partition instanceof DirectoryPartition) { 184 if (((DirectoryPartition)partition).getDirectoryId() == id) { 185 return i; 186 } 187 } 188 } 189 return -1; 190 } 191 192 protected DirectoryPartition getDirectoryById(long id) { 193 int count = getPartitionCount(); 194 for (int i = 0; i < count; i++) { 195 Partition partition = getPartition(i); 196 if (partition instanceof DirectoryPartition) { 197 final DirectoryPartition directoryPartition = (DirectoryPartition) partition; 198 if (directoryPartition.getDirectoryId() == id) { 199 return directoryPartition; 200 } 201 } 202 } 203 return null; 204 } 205 206 public abstract String getContactDisplayName(int position); 207 public abstract void configureLoader(CursorLoader loader, long directoryId); 208 209 /** 210 * Marks all partitions as "loading" 211 */ 212 public void onDataReload() { 213 boolean notify = false; 214 int count = getPartitionCount(); 215 for (int i = 0; i < count; i++) { 216 Partition partition = getPartition(i); 217 if (partition instanceof DirectoryPartition) { 218 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 219 if (!directoryPartition.isLoading()) { 220 notify = true; 221 } 222 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); 223 } 224 } 225 if (notify) { 226 notifyDataSetChanged(); 227 } 228 } 229 230 @Override 231 public void clearPartitions() { 232 int count = getPartitionCount(); 233 for (int i = 0; i < count; i++) { 234 Partition partition = getPartition(i); 235 if (partition instanceof DirectoryPartition) { 236 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 237 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); 238 } 239 } 240 super.clearPartitions(); 241 } 242 243 public boolean isSearchMode() { 244 return mSearchMode; 245 } 246 247 public void setSearchMode(boolean flag) { 248 mSearchMode = flag; 249 } 250 251 public String getQueryString() { 252 return mQueryString; 253 } 254 255 public void setQueryString(String queryString) { 256 mQueryString = queryString; 257 if (TextUtils.isEmpty(queryString)) { 258 mUpperCaseQueryString = null; 259 } else { 260 mUpperCaseQueryString = SearchUtil 261 .cleanStartAndEndOfSearchQuery(queryString.toUpperCase()) ; 262 } 263 } 264 265 public String getUpperCaseQueryString() { 266 return mUpperCaseQueryString; 267 } 268 269 public int getDirectorySearchMode() { 270 return mDirectorySearchMode; 271 } 272 273 public void setDirectorySearchMode(int mode) { 274 mDirectorySearchMode = mode; 275 } 276 277 public int getDirectoryResultLimit() { 278 return mDirectoryResultLimit; 279 } 280 281 public int getDirectoryResultLimit(DirectoryPartition directoryPartition) { 282 final int limit = directoryPartition.getResultLimit(); 283 return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit; 284 } 285 286 public void setDirectoryResultLimit(int limit) { 287 this.mDirectoryResultLimit = limit; 288 } 289 290 public int getContactNameDisplayOrder() { 291 return mDisplayOrder; 292 } 293 294 public void setContactNameDisplayOrder(int displayOrder) { 295 mDisplayOrder = displayOrder; 296 } 297 298 public int getSortOrder() { 299 return mSortOrder; 300 } 301 302 public void setSortOrder(int sortOrder) { 303 mSortOrder = sortOrder; 304 } 305 306 public void setPhotoLoader(ContactPhotoManager photoLoader) { 307 mPhotoLoader = photoLoader; 308 } 309 310 protected ContactPhotoManager getPhotoLoader() { 311 return mPhotoLoader; 312 } 313 314 public boolean getDisplayPhotos() { 315 return mDisplayPhotos; 316 } 317 318 public void setDisplayPhotos(boolean displayPhotos) { 319 mDisplayPhotos = displayPhotos; 320 } 321 322 public boolean getCircularPhotos() { 323 return mCircularPhotos; 324 } 325 326 public void setCircularPhotos(boolean circularPhotos) { 327 mCircularPhotos = circularPhotos; 328 } 329 330 public boolean isEmptyListEnabled() { 331 return mEmptyListEnabled; 332 } 333 334 public void setEmptyListEnabled(boolean flag) { 335 mEmptyListEnabled = flag; 336 } 337 338 public boolean isSelectionVisible() { 339 return mSelectionVisible; 340 } 341 342 public void setSelectionVisible(boolean flag) { 343 this.mSelectionVisible = flag; 344 } 345 346 public boolean isQuickContactEnabled() { 347 return mQuickContactEnabled; 348 } 349 350 public void setQuickContactEnabled(boolean quickContactEnabled) { 351 mQuickContactEnabled = quickContactEnabled; 352 } 353 354 public boolean isAdjustSelectionBoundsEnabled() { 355 return mAdjustSelectionBoundsEnabled; 356 } 357 358 public void setAdjustSelectionBoundsEnabled(boolean enabled) { 359 mAdjustSelectionBoundsEnabled = enabled; 360 } 361 362 public boolean shouldIncludeFavorites() { 363 return mIncludeFavorites; 364 } 365 366 public void setIncludeFavorites(boolean includeFavorites) { 367 mIncludeFavorites = includeFavorites; 368 } 369 370 public void setFavoritesSectionHeader(int numberOfFavorites) { 371 if (mIncludeFavorites) { 372 mNumberOfFavorites = numberOfFavorites; 373 setSectionHeader(numberOfFavorites); 374 } 375 } 376 377 public int getNumberOfFavorites() { 378 return mNumberOfFavorites; 379 } 380 381 private void setSectionHeader(int numberOfItems) { 382 SectionIndexer indexer = getIndexer(); 383 if (indexer != null) { 384 ((ContactsSectionIndexer) indexer).setFavoritesHeader(numberOfItems); 385 } 386 } 387 388 public void setDarkTheme(boolean value) { 389 mDarkTheme = value; 390 } 391 392 /** 393 * Updates partitions according to the directory meta-data contained in the supplied 394 * cursor. 395 */ 396 public void changeDirectories(Cursor cursor) { 397 if (cursor.getCount() == 0) { 398 // Directory table must have at least local directory, without which this adapter will 399 // enter very weird state. 400 Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " + 401 "no directory entries.", new RuntimeException()); 402 return; 403 } 404 HashSet<Long> directoryIds = new HashSet<Long>(); 405 406 int idColumnIndex = cursor.getColumnIndex(Directory._ID); 407 int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE); 408 int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME); 409 int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT); 410 411 // TODO preserve the order of partition to match those of the cursor 412 // Phase I: add new directories 413 cursor.moveToPosition(-1); 414 while (cursor.moveToNext()) { 415 long id = cursor.getLong(idColumnIndex); 416 directoryIds.add(id); 417 if (getPartitionByDirectoryId(id) == -1) { 418 DirectoryPartition partition = new DirectoryPartition(false, true); 419 partition.setDirectoryId(id); 420 if (DirectoryCompat.isRemoteDirectoryId(id)) { 421 if (DirectoryCompat.isEnterpriseDirectoryId(id)) { 422 partition.setLabel(mContext.getString(R.string.directory_search_label_work)); 423 } else { 424 partition.setLabel(mContext.getString(R.string.directory_search_label)); 425 } 426 } else { 427 if (DirectoryCompat.isEnterpriseDirectoryId(id)) { 428 partition.setLabel(mContext.getString(R.string.list_filter_phones_work)); 429 } else { 430 partition.setLabel(mDefaultFilterHeaderText.toString()); 431 } 432 } 433 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex)); 434 partition.setDisplayName(cursor.getString(displayNameColumnIndex)); 435 int photoSupport = cursor.getInt(photoSupportColumnIndex); 436 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY 437 || photoSupport == Directory.PHOTO_SUPPORT_FULL); 438 addPartition(partition); 439 } 440 } 441 442 // Phase II: remove deleted directories 443 int count = getPartitionCount(); 444 for (int i = count; --i >= 0; ) { 445 Partition partition = getPartition(i); 446 if (partition instanceof DirectoryPartition) { 447 long id = ((DirectoryPartition)partition).getDirectoryId(); 448 if (!directoryIds.contains(id)) { 449 removePartition(i); 450 } 451 } 452 } 453 454 invalidate(); 455 notifyDataSetChanged(); 456 } 457 458 @Override 459 public void changeCursor(int partitionIndex, Cursor cursor) { 460 if (partitionIndex >= getPartitionCount()) { 461 // There is no partition for this data 462 return; 463 } 464 465 Partition partition = getPartition(partitionIndex); 466 if (partition instanceof DirectoryPartition) { 467 ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED); 468 } 469 470 if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) { 471 mPhotoLoader.refreshCache(); 472 } 473 474 super.changeCursor(partitionIndex, cursor); 475 476 if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { 477 updateIndexer(cursor); 478 } 479 480 // When the cursor changes, cancel any pending asynchronous photo loads. 481 mPhotoLoader.cancelPendingRequests(mFragmentRootView); 482 } 483 484 public void changeCursor(Cursor cursor) { 485 changeCursor(0, cursor); 486 } 487 488 /** 489 * Updates the indexer, which is used to produce section headers. 490 */ 491 private void updateIndexer(Cursor cursor) { 492 if (cursor == null || cursor.isClosed()) { 493 setIndexer(null); 494 return; 495 } 496 497 Bundle bundle = cursor.getExtras(); 498 if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) && 499 bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) { 500 String sections[] = 501 bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); 502 int counts[] = bundle.getIntArray( 503 Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); 504 505 if (getExtraStartingSection()) { 506 // Insert an additional unnamed section at the top of the list. 507 String allSections[] = new String[sections.length + 1]; 508 int allCounts[] = new int[counts.length + 1]; 509 for (int i = 0; i < sections.length; i++) { 510 allSections[i + 1] = sections[i]; 511 allCounts[i + 1] = counts[i]; 512 } 513 allCounts[0] = 1; 514 allSections[0] = ""; 515 setIndexer(new ContactsSectionIndexer(allSections, allCounts)); 516 } else { 517 setIndexer(new ContactsSectionIndexer(sections, counts)); 518 } 519 } else { 520 setIndexer(null); 521 } 522 } 523 524 protected boolean getExtraStartingSection() { 525 return false; 526 } 527 528 @Override 529 public int getViewTypeCount() { 530 // We need a separate view type for each item type, plus another one for 531 // each type with header, plus one for "other". 532 return getItemViewTypeCount() * 2 + 1; 533 } 534 535 @Override 536 public int getItemViewType(int partitionIndex, int position) { 537 int type = super.getItemViewType(partitionIndex, position); 538 if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { 539 Placement placement = getItemPlacementInSection(position); 540 return placement.firstInSection ? type : getItemViewTypeCount() + type; 541 } else { 542 return type; 543 } 544 } 545 546 @Override 547 public boolean isEmpty() { 548 // TODO 549 // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) { 550 // return true; 551 // } 552 553 if (!mEmptyListEnabled) { 554 return false; 555 } else if (isSearchMode()) { 556 return TextUtils.isEmpty(getQueryString()); 557 } else { 558 return super.isEmpty(); 559 } 560 } 561 562 public boolean isLoading() { 563 int count = getPartitionCount(); 564 for (int i = 0; i < count; i++) { 565 Partition partition = getPartition(i); 566 if (partition instanceof DirectoryPartition 567 && ((DirectoryPartition) partition).isLoading()) { 568 return true; 569 } 570 } 571 return false; 572 } 573 574 public boolean areAllPartitionsEmpty() { 575 int count = getPartitionCount(); 576 for (int i = 0; i < count; i++) { 577 if (!isPartitionEmpty(i)) { 578 return false; 579 } 580 } 581 return true; 582 } 583 584 /** 585 * Changes visibility parameters for the default directory partition. 586 */ 587 public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) { 588 int defaultPartitionIndex = -1; 589 int count = getPartitionCount(); 590 for (int i = 0; i < count; i++) { 591 Partition partition = getPartition(i); 592 if (partition instanceof DirectoryPartition && 593 ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) { 594 defaultPartitionIndex = i; 595 break; 596 } 597 } 598 if (defaultPartitionIndex != -1) { 599 setShowIfEmpty(defaultPartitionIndex, showIfEmpty); 600 setHasHeader(defaultPartitionIndex, hasHeader); 601 } 602 } 603 604 @Override 605 protected View newHeaderView(Context context, int partition, Cursor cursor, 606 ViewGroup parent) { 607 LayoutInflater inflater = LayoutInflater.from(context); 608 View view = inflater.inflate(R.layout.directory_header, parent, false); 609 if (!getPinnedPartitionHeadersEnabled()) { 610 // If the headers are unpinned, there is no need for their background 611 // color to be non-transparent. Setting this transparent reduces maintenance for 612 // non-pinned headers. We don't need to bother synchronizing the activity's 613 // background color with the header background color. 614 view.setBackground(null); 615 } 616 return view; 617 } 618 619 protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) { 620 final Partition partition = getPartition(partitionId); 621 if (partition instanceof DirectoryPartition) { 622 final DirectoryPartition directoryPartition = (DirectoryPartition) partition; 623 final long directoryId = directoryPartition.getDirectoryId(); 624 final long userType = ContactsUtils.determineUserType(directoryId, null); 625 view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK); 626 } 627 } 628 629 @Override 630 protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) { 631 Partition partition = getPartition(partitionIndex); 632 if (!(partition instanceof DirectoryPartition)) { 633 return; 634 } 635 636 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 637 long directoryId = directoryPartition.getDirectoryId(); 638 TextView labelTextView = (TextView)view.findViewById(R.id.label); 639 TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name); 640 labelTextView.setText(directoryPartition.getLabel()); 641 if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) { 642 displayNameTextView.setText(null); 643 } else { 644 String directoryName = directoryPartition.getDisplayName(); 645 String displayName = !TextUtils.isEmpty(directoryName) 646 ? directoryName 647 : directoryPartition.getDirectoryType(); 648 displayNameTextView.setText(displayName); 649 } 650 651 final Resources res = getContext().getResources(); 652 final int headerPaddingTop = partitionIndex == 1 && getPartition(0).isEmpty()? 653 0 : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding); 654 // There should be no extra padding at the top of the first directory header 655 view.setPaddingRelative(view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), 656 view.getPaddingBottom()); 657 } 658 659 // Default implementation simply returns number of rows in the cursor. 660 // Broken out into its own routine so can be overridden by child classes 661 // for eg number of unique contacts for a phone list. 662 protected int getResultCount(Cursor cursor) { 663 return cursor == null ? 0 : cursor.getCount(); 664 } 665 666 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly 667 public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { 668 if (count == 0) { 669 return getContext().getString(zeroResourceId); 670 } else { 671 String format = getContext().getResources() 672 .getQuantityText(pluralResourceId, count).toString(); 673 return String.format(format, count); 674 } 675 } 676 677 public boolean isPhotoSupported(int partitionIndex) { 678 Partition partition = getPartition(partitionIndex); 679 if (partition instanceof DirectoryPartition) { 680 return ((DirectoryPartition) partition).isPhotoSupported(); 681 } 682 return true; 683 } 684 685 /** 686 * Returns the currently selected filter. 687 */ 688 public ContactListFilter getFilter() { 689 return mFilter; 690 } 691 692 public void setFilter(ContactListFilter filter) { 693 mFilter = filter; 694 } 695 696 // TODO: move sharable logic (bindXX() methods) to here with extra arguments 697 698 /** 699 * Loads the photo for the quick contact view and assigns the contact uri. 700 * @param photoIdColumn Index of the photo id column 701 * @param photoUriColumn Index of the photo uri column. Optional: Can be -1 702 * @param contactIdColumn Index of the contact id column 703 * @param lookUpKeyColumn Index of the lookup key column 704 * @param displayNameColumn Index of the display name column 705 */ 706 protected void bindQuickContact(final ContactListItemView view, int partitionIndex, 707 Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, 708 int lookUpKeyColumn, int displayNameColumn) { 709 long photoId = 0; 710 if (!cursor.isNull(photoIdColumn)) { 711 photoId = cursor.getLong(photoIdColumn); 712 } 713 714 QuickContactBadge quickContact = view.getQuickContact(); 715 quickContact.assignContactUri( 716 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn)); 717 if (CompatUtils.hasPrioritizedMimeType()) { 718 // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume 719 // that only Dialer will use this QuickContact badge. This means prioritizing the phone 720 // mimetype here is reasonable. 721 quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); 722 } 723 724 if (photoId != 0 || photoUriColumn == -1) { 725 getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, 726 null); 727 } else { 728 final String photoUriString = cursor.getString(photoUriColumn); 729 final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); 730 DefaultImageRequest request = null; 731 if (photoUri == null) { 732 request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, 733 lookUpKeyColumn); 734 } 735 getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, 736 request); 737 } 738 739 } 740 741 @Override 742 public boolean hasStableIds() { 743 // Whenever bindViewId() is called, the values passed into setId() are stable or 744 // stable-ish. For example, when one contact is modified we don't expect a second 745 // contact's Contact._ID values to change. 746 return true; 747 } 748 749 protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) { 750 // Set a semi-stable id, so that talkback won't get confused when the list gets 751 // refreshed. There is little harm in inserting the same ID twice. 752 long contactId = cursor.getLong(idColumn); 753 view.setId((int) (contactId % Integer.MAX_VALUE)); 754 755 } 756 757 protected Uri getContactUri(int partitionIndex, Cursor cursor, 758 int contactIdColumn, int lookUpKeyColumn) { 759 long contactId = cursor.getLong(contactIdColumn); 760 String lookupKey = cursor.getString(lookUpKeyColumn); 761 long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId(); 762 Uri uri = Contacts.getLookupUri(contactId, lookupKey); 763 if (uri != null && directoryId != Directory.DEFAULT) { 764 uri = uri.buildUpon().appendQueryParameter( 765 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build(); 766 } 767 return uri; 768 } 769 770 /** 771 * Retrieves the lookup key and display name from a cursor, and returns a 772 * {@link DefaultImageRequest} containing these contact details 773 * 774 * @param cursor Contacts cursor positioned at the current row to retrieve contact details for 775 * @param displayNameColumn Column index of the display name 776 * @param lookupKeyColumn Column index of the lookup key 777 * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the 778 * display name and lookup key of the contact. 779 */ 780 public DefaultImageRequest getDefaultImageRequestFromCursor(Cursor cursor, 781 int displayNameColumn, int lookupKeyColumn) { 782 final String displayName = cursor.getString(displayNameColumn); 783 final String lookupKey = cursor.getString(lookupKeyColumn); 784 return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos); 785 } 786 } 787