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