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