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