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.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.Data; 28 import android.provider.ContactsContract.Directory; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.QuickContactBadge; 35 import android.widget.SectionIndexer; 36 import android.widget.TextView; 37 38 import com.android.contacts.common.ContactPhotoManager; 39 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; 40 import com.android.contacts.common.R; 41 import com.android.contacts.common.util.SearchUtil; 42 43 import java.util.HashSet; 44 45 /** 46 * Common base class for various contact-related lists, e.g. contact list, phone number list 47 * etc. 48 */ 49 public abstract class ContactEntryListAdapter extends IndexerListAdapter { 50 51 private static final String TAG = "ContactEntryListAdapter"; 52 53 /** 54 * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should 55 * be included in the search. 56 */ 57 public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false; 58 59 private int mDisplayOrder; 60 private int mSortOrder; 61 62 private boolean mDisplayPhotos; 63 private boolean mCircularPhotos = true; 64 private boolean mQuickContactEnabled; 65 private boolean mAdjustSelectionBoundsEnabled; 66 67 /** 68 * indicates if contact queries include profile 69 */ 70 private boolean mIncludeProfile; 71 72 /** 73 * indicates if query results includes a profile 74 */ 75 private boolean mProfileExists; 76 77 /** 78 * The root view of the fragment that this adapter is associated with. 79 */ 80 private View mFragmentRootView; 81 82 private ContactPhotoManager mPhotoLoader; 83 84 private String mQueryString; 85 private String mUpperCaseQueryString; 86 private boolean mSearchMode; 87 private int mDirectorySearchMode; 88 private int mDirectoryResultLimit = Integer.MAX_VALUE; 89 90 private boolean mEmptyListEnabled = true; 91 92 private boolean mSelectionVisible; 93 94 private ContactListFilter mFilter; 95 private boolean mDarkTheme = false; 96 97 /** Resource used to provide header-text for default filter. */ 98 private CharSequence mDefaultFilterHeaderText; 99 100 public ContactEntryListAdapter(Context context) { 101 super(context); 102 setDefaultFilterHeaderText(R.string.local_search_label); 103 addPartitions(); 104 } 105 106 /** 107 * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of 108 * image loading requests that get cancelled on cursor changes. 109 */ 110 protected void setFragmentRootView(View fragmentRootView) { 111 mFragmentRootView = fragmentRootView; 112 } 113 114 protected void setDefaultFilterHeaderText(int resourceId) { 115 mDefaultFilterHeaderText = getContext().getResources().getText(resourceId); 116 } 117 118 @Override 119 protected ContactListItemView newView( 120 Context context, int partition, Cursor cursor, int position, ViewGroup parent) { 121 final ContactListItemView view = new ContactListItemView(context, null); 122 view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); 123 view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); 124 return view; 125 } 126 127 @Override 128 protected void bindView(View itemView, int partition, Cursor cursor, int position) { 129 final ContactListItemView view = (ContactListItemView) itemView; 130 view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); 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 shouldIncludeProfile() { 363 return mIncludeProfile; 364 } 365 366 public void setIncludeProfile(boolean includeProfile) { 367 mIncludeProfile = includeProfile; 368 } 369 370 public void setProfileExists(boolean exists) { 371 mProfileExists = exists; 372 // Stick the "ME" header for the profile 373 if (exists) { 374 SectionIndexer indexer = getIndexer(); 375 if (indexer != null) { 376 ((ContactsSectionIndexer) indexer).setProfileHeader( 377 getContext().getString(R.string.user_profile_contacts_list_header)); 378 } 379 } 380 } 381 382 public boolean hasProfile() { 383 return mProfileExists; 384 } 385 386 public void setDarkTheme(boolean value) { 387 mDarkTheme = value; 388 } 389 390 /** 391 * Updates partitions according to the directory meta-data contained in the supplied 392 * cursor. 393 */ 394 public void changeDirectories(Cursor cursor) { 395 if (cursor.getCount() == 0) { 396 // Directory table must have at least local directory, without which this adapter will 397 // enter very weird state. 398 Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " + 399 "no directory entries.", new RuntimeException()); 400 return; 401 } 402 HashSet<Long> directoryIds = new HashSet<Long>(); 403 404 int idColumnIndex = cursor.getColumnIndex(Directory._ID); 405 int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE); 406 int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME); 407 int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT); 408 409 // TODO preserve the order of partition to match those of the cursor 410 // Phase I: add new directories 411 cursor.moveToPosition(-1); 412 while (cursor.moveToNext()) { 413 long id = cursor.getLong(idColumnIndex); 414 directoryIds.add(id); 415 if (getPartitionByDirectoryId(id) == -1) { 416 DirectoryPartition partition = new DirectoryPartition(false, true); 417 partition.setDirectoryId(id); 418 if (isRemoteDirectory(id)) { 419 partition.setLabel(mContext.getString(R.string.directory_search_label)); 420 } else { 421 partition.setLabel(mDefaultFilterHeaderText.toString()); 422 } 423 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex)); 424 partition.setDisplayName(cursor.getString(displayNameColumnIndex)); 425 int photoSupport = cursor.getInt(photoSupportColumnIndex); 426 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY 427 || photoSupport == Directory.PHOTO_SUPPORT_FULL); 428 addPartition(partition); 429 } 430 } 431 432 // Phase II: remove deleted directories 433 int count = getPartitionCount(); 434 for (int i = count; --i >= 0; ) { 435 Partition partition = getPartition(i); 436 if (partition instanceof DirectoryPartition) { 437 long id = ((DirectoryPartition)partition).getDirectoryId(); 438 if (!directoryIds.contains(id)) { 439 removePartition(i); 440 } 441 } 442 } 443 444 invalidate(); 445 notifyDataSetChanged(); 446 } 447 448 @Override 449 public void changeCursor(int partitionIndex, Cursor cursor) { 450 if (partitionIndex >= getPartitionCount()) { 451 // There is no partition for this data 452 return; 453 } 454 455 Partition partition = getPartition(partitionIndex); 456 if (partition instanceof DirectoryPartition) { 457 ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED); 458 } 459 460 if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) { 461 mPhotoLoader.refreshCache(); 462 } 463 464 super.changeCursor(partitionIndex, cursor); 465 466 if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { 467 updateIndexer(cursor); 468 } 469 470 // When the cursor changes, cancel any pending asynchronous photo loads. 471 mPhotoLoader.cancelPendingRequests(mFragmentRootView); 472 } 473 474 public void changeCursor(Cursor cursor) { 475 changeCursor(0, cursor); 476 } 477 478 /** 479 * Updates the indexer, which is used to produce section headers. 480 */ 481 private void updateIndexer(Cursor cursor) { 482 if (cursor == null) { 483 setIndexer(null); 484 return; 485 } 486 487 Bundle bundle = cursor.getExtras(); 488 if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) && 489 bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) { 490 String sections[] = 491 bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); 492 int counts[] = bundle.getIntArray( 493 Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); 494 495 if (getExtraStartingSection()) { 496 // Insert an additional unnamed section at the top of the list. 497 String allSections[] = new String[sections.length + 1]; 498 int allCounts[] = new int[counts.length + 1]; 499 for (int i = 0; i < sections.length; i++) { 500 allSections[i + 1] = sections[i]; 501 allCounts[i + 1] = counts[i]; 502 } 503 allCounts[0] = 1; 504 allSections[0] = ""; 505 setIndexer(new ContactsSectionIndexer(allSections, allCounts)); 506 } else { 507 setIndexer(new ContactsSectionIndexer(sections, counts)); 508 } 509 } else { 510 setIndexer(null); 511 } 512 } 513 514 protected boolean getExtraStartingSection() { 515 return false; 516 } 517 518 @Override 519 public int getViewTypeCount() { 520 // We need a separate view type for each item type, plus another one for 521 // each type with header, plus one for "other". 522 return getItemViewTypeCount() * 2 + 1; 523 } 524 525 @Override 526 public int getItemViewType(int partitionIndex, int position) { 527 int type = super.getItemViewType(partitionIndex, position); 528 if (!isUserProfile(position) 529 && isSectionHeaderDisplayEnabled() 530 && partitionIndex == getIndexedPartition()) { 531 Placement placement = getItemPlacementInSection(position); 532 return placement.firstInSection ? type : getItemViewTypeCount() + type; 533 } else { 534 return type; 535 } 536 } 537 538 @Override 539 public boolean isEmpty() { 540 // TODO 541 // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) { 542 // return true; 543 // } 544 545 if (!mEmptyListEnabled) { 546 return false; 547 } else if (isSearchMode()) { 548 return TextUtils.isEmpty(getQueryString()); 549 } else { 550 return super.isEmpty(); 551 } 552 } 553 554 public boolean isLoading() { 555 int count = getPartitionCount(); 556 for (int i = 0; i < count; i++) { 557 Partition partition = getPartition(i); 558 if (partition instanceof DirectoryPartition 559 && ((DirectoryPartition) partition).isLoading()) { 560 return true; 561 } 562 } 563 return false; 564 } 565 566 public boolean areAllPartitionsEmpty() { 567 int count = getPartitionCount(); 568 for (int i = 0; i < count; i++) { 569 if (!isPartitionEmpty(i)) { 570 return false; 571 } 572 } 573 return true; 574 } 575 576 /** 577 * Changes visibility parameters for the default directory partition. 578 */ 579 public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) { 580 int defaultPartitionIndex = -1; 581 int count = getPartitionCount(); 582 for (int i = 0; i < count; i++) { 583 Partition partition = getPartition(i); 584 if (partition instanceof DirectoryPartition && 585 ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) { 586 defaultPartitionIndex = i; 587 break; 588 } 589 } 590 if (defaultPartitionIndex != -1) { 591 setShowIfEmpty(defaultPartitionIndex, showIfEmpty); 592 setHasHeader(defaultPartitionIndex, hasHeader); 593 } 594 } 595 596 @Override 597 protected View newHeaderView(Context context, int partition, Cursor cursor, 598 ViewGroup parent) { 599 LayoutInflater inflater = LayoutInflater.from(context); 600 View view = inflater.inflate(R.layout.directory_header, parent, false); 601 if (!getPinnedPartitionHeadersEnabled()) { 602 // If the headers are unpinned, there is no need for their background 603 // color to be non-transparent. Setting this transparent reduces maintenance for 604 // non-pinned headers. We don't need to bother synchronizing the activity's 605 // background color with the header background color. 606 view.setBackground(null); 607 } 608 return view; 609 } 610 611 @Override 612 protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) { 613 Partition partition = getPartition(partitionIndex); 614 if (!(partition instanceof DirectoryPartition)) { 615 return; 616 } 617 618 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 619 long directoryId = directoryPartition.getDirectoryId(); 620 TextView labelTextView = (TextView)view.findViewById(R.id.label); 621 TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name); 622 labelTextView.setText(directoryPartition.getLabel()); 623 if (!isRemoteDirectory(directoryId)) { 624 displayNameTextView.setText(null); 625 } else { 626 String directoryName = directoryPartition.getDisplayName(); 627 String displayName = !TextUtils.isEmpty(directoryName) 628 ? directoryName 629 : directoryPartition.getDirectoryType(); 630 displayNameTextView.setText(displayName); 631 } 632 633 final Resources res = getContext().getResources(); 634 final int headerPaddingTop = partitionIndex == 1 && getPartition(0).isEmpty()? 635 0 : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding); 636 // There should be no extra padding at the top of the first directory header 637 view.setPaddingRelative(view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), 638 view.getPaddingBottom()); 639 } 640 641 // Default implementation simply returns number of rows in the cursor. 642 // Broken out into its own routine so can be overridden by child classes 643 // for eg number of unique contacts for a phone list. 644 protected int getResultCount(Cursor cursor) { 645 return cursor == null ? 0 : cursor.getCount(); 646 } 647 648 /** 649 * Checks whether the contact entry at the given position represents the user's profile. 650 */ 651 protected boolean isUserProfile(int position) { 652 // The profile only ever appears in the first position if it is present. So if the position 653 // is anything beyond 0, it can't be the profile. 654 boolean isUserProfile = false; 655 if (position == 0) { 656 int partition = getPartitionForPosition(position); 657 if (partition >= 0) { 658 // Save the old cursor position - the call to getItem() may modify the cursor 659 // position. 660 int offset = getCursor(partition).getPosition(); 661 Cursor cursor = (Cursor) getItem(position); 662 if (cursor != null) { 663 int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE); 664 if (profileColumnIndex != -1) { 665 isUserProfile = cursor.getInt(profileColumnIndex) == 1; 666 } 667 // Restore the old cursor position. 668 cursor.moveToPosition(offset); 669 } 670 } 671 } 672 return isUserProfile; 673 } 674 675 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly 676 public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { 677 if (count == 0) { 678 return getContext().getString(zeroResourceId); 679 } else { 680 String format = getContext().getResources() 681 .getQuantityText(pluralResourceId, count).toString(); 682 return String.format(format, count); 683 } 684 } 685 686 public boolean isPhotoSupported(int partitionIndex) { 687 Partition partition = getPartition(partitionIndex); 688 if (partition instanceof DirectoryPartition) { 689 return ((DirectoryPartition) partition).isPhotoSupported(); 690 } 691 return true; 692 } 693 694 /** 695 * Returns the currently selected filter. 696 */ 697 public ContactListFilter getFilter() { 698 return mFilter; 699 } 700 701 public void setFilter(ContactListFilter filter) { 702 mFilter = filter; 703 } 704 705 // TODO: move sharable logic (bindXX() methods) to here with extra arguments 706 707 /** 708 * Loads the photo for the quick contact view and assigns the contact uri. 709 * @param photoIdColumn Index of the photo id column 710 * @param photoUriColumn Index of the photo uri column. Optional: Can be -1 711 * @param contactIdColumn Index of the contact id column 712 * @param lookUpKeyColumn Index of the lookup key column 713 * @param displayNameColumn Index of the display name column 714 */ 715 protected void bindQuickContact(final ContactListItemView view, int partitionIndex, 716 Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, 717 int lookUpKeyColumn, int displayNameColumn) { 718 long photoId = 0; 719 if (!cursor.isNull(photoIdColumn)) { 720 photoId = cursor.getLong(photoIdColumn); 721 } 722 723 QuickContactBadge quickContact = view.getQuickContact(); 724 quickContact.assignContactUri( 725 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn)); 726 // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume 727 // that only Dialer will use this QuickContact badge. This means prioritizing the phone 728 // mimetype here is reasonable. 729 quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); 730 731 if (photoId != 0 || photoUriColumn == -1) { 732 getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, 733 null); 734 } else { 735 final String photoUriString = cursor.getString(photoUriColumn); 736 final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); 737 DefaultImageRequest request = null; 738 if (photoUri == null) { 739 request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, 740 lookUpKeyColumn); 741 } 742 getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, 743 request); 744 } 745 746 } 747 748 @Override 749 public boolean hasStableIds() { 750 // Whenever bindViewId() is called, the values passed into setId() are stable or 751 // stable-ish. For example, when one contact is modified we don't expect a second 752 // contact's Contact._ID values to change. 753 return true; 754 } 755 756 protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) { 757 // Set a semi-stable id, so that talkback won't get confused when the list gets 758 // refreshed. There is little harm in inserting the same ID twice. 759 long contactId = cursor.getLong(idColumn); 760 view.setId((int) (contactId % Integer.MAX_VALUE)); 761 762 } 763 764 protected Uri getContactUri(int partitionIndex, Cursor cursor, 765 int contactIdColumn, int lookUpKeyColumn) { 766 long contactId = cursor.getLong(contactIdColumn); 767 String lookupKey = cursor.getString(lookUpKeyColumn); 768 long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId(); 769 Uri uri = Contacts.getLookupUri(contactId, lookupKey); 770 if (uri != null && directoryId != Directory.DEFAULT) { 771 uri = uri.buildUpon().appendQueryParameter( 772 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build(); 773 } 774 return uri; 775 } 776 777 public static boolean isRemoteDirectory(long directoryId) { 778 return directoryId != Directory.DEFAULT 779 && directoryId != Directory.LOCAL_INVISIBLE; 780 } 781 782 /** 783 * Retrieves the lookup key and display name from a cursor, and returns a 784 * {@link DefaultImageRequest} containing these contact details 785 * 786 * @param cursor Contacts cursor positioned at the current row to retrieve contact details for 787 * @param displayNameColumn Column index of the display name 788 * @param lookupKeyColumn Column index of the lookup key 789 * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the 790 * display name and lookup key of the contact. 791 */ 792 public DefaultImageRequest getDefaultImageRequestFromCursor(Cursor cursor, 793 int displayNameColumn, int lookupKeyColumn) { 794 final String displayName = cursor.getString(displayNameColumn); 795 final String lookupKey = cursor.getString(lookupKeyColumn); 796 return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos); 797 } 798 } 799