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 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 } 131 132 @Override createPinnedSectionHeaderView(Context context, ViewGroup parent)133 protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) { 134 return new ContactListPinnedHeaderView(context, null, parent); 135 } 136 137 @Override setPinnedSectionTitle(View pinnedHeaderView, String title)138 protected void setPinnedSectionTitle(View pinnedHeaderView, String title) { 139 ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title); 140 } 141 addPartitions()142 protected void addPartitions() { 143 addPartition(createDefaultDirectoryPartition()); 144 } 145 createDefaultDirectoryPartition()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 */ removeDirectoriesAfterDefault()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 getPartitionByDirectoryId(long id)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 getDirectoryById(long id)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 getContactDisplayName(int position)205 public abstract String getContactDisplayName(int position); configureLoader(CursorLoader loader, long directoryId)206 public abstract void configureLoader(CursorLoader loader, long directoryId); 207 208 /** 209 * Marks all partitions as "loading" 210 */ onDataReload()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 clearPartitions()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 isSearchMode()242 public boolean isSearchMode() { 243 return mSearchMode; 244 } 245 setSearchMode(boolean flag)246 public void setSearchMode(boolean flag) { 247 mSearchMode = flag; 248 } 249 getQueryString()250 public String getQueryString() { 251 return mQueryString; 252 } 253 setQueryString(String queryString)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 getUpperCaseQueryString()264 public String getUpperCaseQueryString() { 265 return mUpperCaseQueryString; 266 } 267 getDirectorySearchMode()268 public int getDirectorySearchMode() { 269 return mDirectorySearchMode; 270 } 271 setDirectorySearchMode(int mode)272 public void setDirectorySearchMode(int mode) { 273 mDirectorySearchMode = mode; 274 } 275 getDirectoryResultLimit()276 public int getDirectoryResultLimit() { 277 return mDirectoryResultLimit; 278 } 279 getDirectoryResultLimit(DirectoryPartition directoryPartition)280 public int getDirectoryResultLimit(DirectoryPartition directoryPartition) { 281 final int limit = directoryPartition.getResultLimit(); 282 return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit; 283 } 284 setDirectoryResultLimit(int limit)285 public void setDirectoryResultLimit(int limit) { 286 this.mDirectoryResultLimit = limit; 287 } 288 getContactNameDisplayOrder()289 public int getContactNameDisplayOrder() { 290 return mDisplayOrder; 291 } 292 setContactNameDisplayOrder(int displayOrder)293 public void setContactNameDisplayOrder(int displayOrder) { 294 mDisplayOrder = displayOrder; 295 } 296 getSortOrder()297 public int getSortOrder() { 298 return mSortOrder; 299 } 300 setSortOrder(int sortOrder)301 public void setSortOrder(int sortOrder) { 302 mSortOrder = sortOrder; 303 } 304 setPhotoLoader(ContactPhotoManager photoLoader)305 public void setPhotoLoader(ContactPhotoManager photoLoader) { 306 mPhotoLoader = photoLoader; 307 } 308 getPhotoLoader()309 protected ContactPhotoManager getPhotoLoader() { 310 return mPhotoLoader; 311 } 312 getDisplayPhotos()313 public boolean getDisplayPhotos() { 314 return mDisplayPhotos; 315 } 316 setDisplayPhotos(boolean displayPhotos)317 public void setDisplayPhotos(boolean displayPhotos) { 318 mDisplayPhotos = displayPhotos; 319 } 320 getCircularPhotos()321 public boolean getCircularPhotos() { 322 return mCircularPhotos; 323 } 324 setCircularPhotos(boolean circularPhotos)325 public void setCircularPhotos(boolean circularPhotos) { 326 mCircularPhotos = circularPhotos; 327 } 328 isEmptyListEnabled()329 public boolean isEmptyListEnabled() { 330 return mEmptyListEnabled; 331 } 332 setEmptyListEnabled(boolean flag)333 public void setEmptyListEnabled(boolean flag) { 334 mEmptyListEnabled = flag; 335 } 336 isSelectionVisible()337 public boolean isSelectionVisible() { 338 return mSelectionVisible; 339 } 340 setSelectionVisible(boolean flag)341 public void setSelectionVisible(boolean flag) { 342 this.mSelectionVisible = flag; 343 } 344 isQuickContactEnabled()345 public boolean isQuickContactEnabled() { 346 return mQuickContactEnabled; 347 } 348 setQuickContactEnabled(boolean quickContactEnabled)349 public void setQuickContactEnabled(boolean quickContactEnabled) { 350 mQuickContactEnabled = quickContactEnabled; 351 } 352 isAdjustSelectionBoundsEnabled()353 public boolean isAdjustSelectionBoundsEnabled() { 354 return mAdjustSelectionBoundsEnabled; 355 } 356 setAdjustSelectionBoundsEnabled(boolean enabled)357 public void setAdjustSelectionBoundsEnabled(boolean enabled) { 358 mAdjustSelectionBoundsEnabled = enabled; 359 } 360 shouldIncludeProfile()361 public boolean shouldIncludeProfile() { 362 return mIncludeProfile; 363 } 364 setIncludeProfile(boolean includeProfile)365 public void setIncludeProfile(boolean includeProfile) { 366 mIncludeProfile = includeProfile; 367 } 368 setProfileExists(boolean exists)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 hasProfile()381 public boolean hasProfile() { 382 return mProfileExists; 383 } 384 setDarkTheme(boolean value)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 */ changeDirectories(Cursor cursor)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 changeCursor(int partitionIndex, Cursor cursor)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 changeCursor(Cursor cursor)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 */ updateIndexer(Cursor cursor)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 getExtraStartingSection()513 protected boolean getExtraStartingSection() { 514 return false; 515 } 516 517 @Override getViewTypeCount()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 getItemViewType(int partitionIndex, int position)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 isEmpty()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 isLoading()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 areAllPartitionsEmpty()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 */ configureDefaultPartition(boolean showIfEmpty, boolean hasHeader)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 newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent)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 bindHeaderView(View view, int partitionIndex, Cursor cursor)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. getResultCount(Cursor cursor)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 */ isUserProfile(int position)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 getQuantityText(int count, int zeroResourceId, int pluralResourceId)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 isPhotoSupported(int partitionIndex)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 */ getFilter()696 public ContactListFilter getFilter() { 697 return mFilter; 698 } 699 setFilter(ContactListFilter filter)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 */ bindQuickContact(final ContactListItemView view, int partitionIndex, Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, int lookUpKeyColumn, int displayNameColumn)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 hasStableIds()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 bindViewId(final ContactListItemView view, Cursor cursor, int idColumn)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 getContactUri(int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn)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 isRemoteDirectory(long directoryId)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 */ getDefaultImageRequestFromCursor(Cursor cursor, int displayNameColumn, int lookupKeyColumn)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