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