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.R; 41 import com.android.contacts.common.util.SearchUtil; 42 43 import java.util.HashSet; 44 45 /** 46 * Common base class for various contact-related lists, e.g. contact list, phone number list 47 * etc. 48 */ 49 public abstract class ContactEntryListAdapter extends IndexerListAdapter { 50 51 private static final String TAG = "ContactEntryListAdapter"; 52 53 /** 54 * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should 55 * be included in the 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 /** 68 * indicates if contact queries include profile 69 */ 70 private boolean mIncludeProfile; 71 72 /** 73 * indicates if query results includes a profile 74 */ 75 private boolean mProfileExists; 76 77 /** 78 * The root view of the fragment that this adapter is associated with. 79 */ 80 private View mFragmentRootView; 81 82 private ContactPhotoManager mPhotoLoader; 83 84 private String mQueryString; 85 private String mUpperCaseQueryString; 86 private boolean mSearchMode; 87 private int mDirectorySearchMode; 88 private int mDirectoryResultLimit = Integer.MAX_VALUE; 89 90 private boolean mEmptyListEnabled = true; 91 92 private boolean mSelectionVisible; 93 94 private ContactListFilter mFilter; 95 private boolean mDarkTheme = false; 96 97 /** Resource used to provide header-text for default filter. */ 98 private CharSequence mDefaultFilterHeaderText; 99 ContactEntryListAdapter(Context context)100 public ContactEntryListAdapter(Context context) { 101 super(context); 102 setDefaultFilterHeaderText(R.string.local_search_label); 103 addPartitions(); 104 } 105 106 /** 107 * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of 108 * image loading requests that get cancelled on cursor changes. 109 */ setFragmentRootView(View fragmentRootView)110 protected void setFragmentRootView(View fragmentRootView) { 111 mFragmentRootView = fragmentRootView; 112 } 113 setDefaultFilterHeaderText(int resourceId)114 protected void setDefaultFilterHeaderText(int resourceId) { 115 mDefaultFilterHeaderText = getContext().getResources().getText(resourceId); 116 } 117 118 @Override newView( Context context, int partition, Cursor cursor, int position, ViewGroup parent)119 protected ContactListItemView newView( 120 Context context, int partition, Cursor cursor, int position, ViewGroup parent) { 121 final ContactListItemView view = new ContactListItemView(context, null); 122 view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); 123 view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); 124 return view; 125 } 126 127 @Override bindView(View itemView, int partition, Cursor cursor, int position)128 protected void bindView(View itemView, int partition, Cursor cursor, int position) { 129 final ContactListItemView view = (ContactListItemView) itemView; 130 view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); 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 shouldIncludeProfile()362 public boolean shouldIncludeProfile() { 363 return mIncludeProfile; 364 } 365 setIncludeProfile(boolean includeProfile)366 public void setIncludeProfile(boolean includeProfile) { 367 mIncludeProfile = includeProfile; 368 } 369 setProfileExists(boolean exists)370 public void setProfileExists(boolean exists) { 371 mProfileExists = exists; 372 // Stick the "ME" header for the profile 373 if (exists) { 374 SectionIndexer indexer = getIndexer(); 375 if (indexer != null) { 376 ((ContactsSectionIndexer) indexer).setProfileHeader( 377 getContext().getString(R.string.user_profile_contacts_list_header)); 378 } 379 } 380 } 381 hasProfile()382 public boolean hasProfile() { 383 return mProfileExists; 384 } 385 setDarkTheme(boolean value)386 public void setDarkTheme(boolean value) { 387 mDarkTheme = value; 388 } 389 390 /** 391 * Updates partitions according to the directory meta-data contained in the supplied 392 * cursor. 393 */ changeDirectories(Cursor cursor)394 public void changeDirectories(Cursor cursor) { 395 if (cursor.getCount() == 0) { 396 // Directory table must have at least local directory, without which this adapter will 397 // enter very weird state. 398 Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " + 399 "no directory entries.", new RuntimeException()); 400 return; 401 } 402 HashSet<Long> directoryIds = new HashSet<Long>(); 403 404 int idColumnIndex = cursor.getColumnIndex(Directory._ID); 405 int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE); 406 int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME); 407 int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT); 408 409 // TODO preserve the order of partition to match those of the cursor 410 // Phase I: add new directories 411 cursor.moveToPosition(-1); 412 while (cursor.moveToNext()) { 413 long id = cursor.getLong(idColumnIndex); 414 directoryIds.add(id); 415 if (getPartitionByDirectoryId(id) == -1) { 416 DirectoryPartition partition = new DirectoryPartition(false, true); 417 partition.setDirectoryId(id); 418 if (isRemoteDirectory(id)) { 419 partition.setLabel(mContext.getString(R.string.directory_search_label)); 420 } else { 421 partition.setLabel(mDefaultFilterHeaderText.toString()); 422 } 423 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex)); 424 partition.setDisplayName(cursor.getString(displayNameColumnIndex)); 425 int photoSupport = cursor.getInt(photoSupportColumnIndex); 426 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY 427 || photoSupport == Directory.PHOTO_SUPPORT_FULL); 428 addPartition(partition); 429 } 430 } 431 432 // Phase II: remove deleted directories 433 int count = getPartitionCount(); 434 for (int i = count; --i >= 0; ) { 435 Partition partition = getPartition(i); 436 if (partition instanceof DirectoryPartition) { 437 long id = ((DirectoryPartition)partition).getDirectoryId(); 438 if (!directoryIds.contains(id)) { 439 removePartition(i); 440 } 441 } 442 } 443 444 invalidate(); 445 notifyDataSetChanged(); 446 } 447 448 @Override changeCursor(int partitionIndex, Cursor cursor)449 public void changeCursor(int partitionIndex, Cursor cursor) { 450 if (partitionIndex >= getPartitionCount()) { 451 // There is no partition for this data 452 return; 453 } 454 455 Partition partition = getPartition(partitionIndex); 456 if (partition instanceof DirectoryPartition) { 457 ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED); 458 } 459 460 if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) { 461 mPhotoLoader.refreshCache(); 462 } 463 464 super.changeCursor(partitionIndex, cursor); 465 466 if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { 467 updateIndexer(cursor); 468 } 469 470 // When the cursor changes, cancel any pending asynchronous photo loads. 471 mPhotoLoader.cancelPendingRequests(mFragmentRootView); 472 } 473 changeCursor(Cursor cursor)474 public void changeCursor(Cursor cursor) { 475 changeCursor(0, cursor); 476 } 477 478 /** 479 * Updates the indexer, which is used to produce section headers. 480 */ updateIndexer(Cursor cursor)481 private void updateIndexer(Cursor cursor) { 482 if (cursor == null) { 483 setIndexer(null); 484 return; 485 } 486 487 Bundle bundle = cursor.getExtras(); 488 if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) && 489 bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) { 490 String sections[] = 491 bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); 492 int counts[] = bundle.getIntArray( 493 Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); 494 495 if (getExtraStartingSection()) { 496 // Insert an additional unnamed section at the top of the list. 497 String allSections[] = new String[sections.length + 1]; 498 int allCounts[] = new int[counts.length + 1]; 499 for (int i = 0; i < sections.length; i++) { 500 allSections[i + 1] = sections[i]; 501 allCounts[i + 1] = counts[i]; 502 } 503 allCounts[0] = 1; 504 allSections[0] = ""; 505 setIndexer(new ContactsSectionIndexer(allSections, allCounts)); 506 } else { 507 setIndexer(new ContactsSectionIndexer(sections, counts)); 508 } 509 } else { 510 setIndexer(null); 511 } 512 } 513 getExtraStartingSection()514 protected boolean getExtraStartingSection() { 515 return false; 516 } 517 518 @Override getViewTypeCount()519 public int getViewTypeCount() { 520 // We need a separate view type for each item type, plus another one for 521 // each type with header, plus one for "other". 522 return getItemViewTypeCount() * 2 + 1; 523 } 524 525 @Override getItemViewType(int partitionIndex, int position)526 public int getItemViewType(int partitionIndex, int position) { 527 int type = super.getItemViewType(partitionIndex, position); 528 if (!isUserProfile(position) 529 && isSectionHeaderDisplayEnabled() 530 && partitionIndex == getIndexedPartition()) { 531 Placement placement = getItemPlacementInSection(position); 532 return placement.firstInSection ? type : getItemViewTypeCount() + type; 533 } else { 534 return type; 535 } 536 } 537 538 @Override isEmpty()539 public boolean isEmpty() { 540 // TODO 541 // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) { 542 // return true; 543 // } 544 545 if (!mEmptyListEnabled) { 546 return false; 547 } else if (isSearchMode()) { 548 return TextUtils.isEmpty(getQueryString()); 549 } else { 550 return super.isEmpty(); 551 } 552 } 553 isLoading()554 public boolean isLoading() { 555 int count = getPartitionCount(); 556 for (int i = 0; i < count; i++) { 557 Partition partition = getPartition(i); 558 if (partition instanceof DirectoryPartition 559 && ((DirectoryPartition) partition).isLoading()) { 560 return true; 561 } 562 } 563 return false; 564 } 565 areAllPartitionsEmpty()566 public boolean areAllPartitionsEmpty() { 567 int count = getPartitionCount(); 568 for (int i = 0; i < count; i++) { 569 if (!isPartitionEmpty(i)) { 570 return false; 571 } 572 } 573 return true; 574 } 575 576 /** 577 * Changes visibility parameters for the default directory partition. 578 */ configureDefaultPartition(boolean showIfEmpty, boolean hasHeader)579 public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) { 580 int defaultPartitionIndex = -1; 581 int count = getPartitionCount(); 582 for (int i = 0; i < count; i++) { 583 Partition partition = getPartition(i); 584 if (partition instanceof DirectoryPartition && 585 ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) { 586 defaultPartitionIndex = i; 587 break; 588 } 589 } 590 if (defaultPartitionIndex != -1) { 591 setShowIfEmpty(defaultPartitionIndex, showIfEmpty); 592 setHasHeader(defaultPartitionIndex, hasHeader); 593 } 594 } 595 596 @Override newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent)597 protected View newHeaderView(Context context, int partition, Cursor cursor, 598 ViewGroup parent) { 599 LayoutInflater inflater = LayoutInflater.from(context); 600 View view = inflater.inflate(R.layout.directory_header, parent, false); 601 if (!getPinnedPartitionHeadersEnabled()) { 602 // If the headers are unpinned, there is no need for their background 603 // color to be non-transparent. Setting this transparent reduces maintenance for 604 // non-pinned headers. We don't need to bother synchronizing the activity's 605 // background color with the header background color. 606 view.setBackground(null); 607 } 608 return view; 609 } 610 611 @Override bindHeaderView(View view, int partitionIndex, Cursor cursor)612 protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) { 613 Partition partition = getPartition(partitionIndex); 614 if (!(partition instanceof DirectoryPartition)) { 615 return; 616 } 617 618 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 619 long directoryId = directoryPartition.getDirectoryId(); 620 TextView labelTextView = (TextView)view.findViewById(R.id.label); 621 TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name); 622 labelTextView.setText(directoryPartition.getLabel()); 623 if (!isRemoteDirectory(directoryId)) { 624 displayNameTextView.setText(null); 625 } else { 626 String directoryName = directoryPartition.getDisplayName(); 627 String displayName = !TextUtils.isEmpty(directoryName) 628 ? directoryName 629 : directoryPartition.getDirectoryType(); 630 displayNameTextView.setText(displayName); 631 } 632 633 final Resources res = getContext().getResources(); 634 final int headerPaddingTop = partitionIndex == 1 && getPartition(0).isEmpty()? 635 0 : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding); 636 // There should be no extra padding at the top of the first directory header 637 view.setPaddingRelative(view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), 638 view.getPaddingBottom()); 639 } 640 641 // Default implementation simply returns number of rows in the cursor. 642 // Broken out into its own routine so can be overridden by child classes 643 // for eg number of unique contacts for a phone list. getResultCount(Cursor cursor)644 protected int getResultCount(Cursor cursor) { 645 return cursor == null ? 0 : cursor.getCount(); 646 } 647 648 /** 649 * Checks whether the contact entry at the given position represents the user's profile. 650 */ isUserProfile(int position)651 protected boolean isUserProfile(int position) { 652 // The profile only ever appears in the first position if it is present. So if the position 653 // is anything beyond 0, it can't be the profile. 654 boolean isUserProfile = false; 655 if (position == 0) { 656 int partition = getPartitionForPosition(position); 657 if (partition >= 0) { 658 // Save the old cursor position - the call to getItem() may modify the cursor 659 // position. 660 int offset = getCursor(partition).getPosition(); 661 Cursor cursor = (Cursor) getItem(position); 662 if (cursor != null) { 663 int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE); 664 if (profileColumnIndex != -1) { 665 isUserProfile = cursor.getInt(profileColumnIndex) == 1; 666 } 667 // Restore the old cursor position. 668 cursor.moveToPosition(offset); 669 } 670 } 671 } 672 return isUserProfile; 673 } 674 675 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly getQuantityText(int count, int zeroResourceId, int pluralResourceId)676 public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { 677 if (count == 0) { 678 return getContext().getString(zeroResourceId); 679 } else { 680 String format = getContext().getResources() 681 .getQuantityText(pluralResourceId, count).toString(); 682 return String.format(format, count); 683 } 684 } 685 isPhotoSupported(int partitionIndex)686 public boolean isPhotoSupported(int partitionIndex) { 687 Partition partition = getPartition(partitionIndex); 688 if (partition instanceof DirectoryPartition) { 689 return ((DirectoryPartition) partition).isPhotoSupported(); 690 } 691 return true; 692 } 693 694 /** 695 * Returns the currently selected filter. 696 */ getFilter()697 public ContactListFilter getFilter() { 698 return mFilter; 699 } 700 setFilter(ContactListFilter filter)701 public void setFilter(ContactListFilter filter) { 702 mFilter = filter; 703 } 704 705 // TODO: move sharable logic (bindXX() methods) to here with extra arguments 706 707 /** 708 * Loads the photo for the quick contact view and assigns the contact uri. 709 * @param photoIdColumn Index of the photo id column 710 * @param photoUriColumn Index of the photo uri column. Optional: Can be -1 711 * @param contactIdColumn Index of the contact id column 712 * @param lookUpKeyColumn Index of the lookup key column 713 * @param displayNameColumn Index of the display name column 714 */ bindQuickContact(final ContactListItemView view, int partitionIndex, Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, int lookUpKeyColumn, int displayNameColumn)715 protected void bindQuickContact(final ContactListItemView view, int partitionIndex, 716 Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, 717 int lookUpKeyColumn, int displayNameColumn) { 718 long photoId = 0; 719 if (!cursor.isNull(photoIdColumn)) { 720 photoId = cursor.getLong(photoIdColumn); 721 } 722 723 QuickContactBadge quickContact = view.getQuickContact(); 724 quickContact.assignContactUri( 725 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn)); 726 // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume 727 // that only Dialer will use this QuickContact badge. This means prioritizing the phone 728 // mimetype here is reasonable. 729 quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); 730 731 if (photoId != 0 || photoUriColumn == -1) { 732 getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, 733 null); 734 } else { 735 final String photoUriString = cursor.getString(photoUriColumn); 736 final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); 737 DefaultImageRequest request = null; 738 if (photoUri == null) { 739 request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, 740 lookUpKeyColumn); 741 } 742 getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, 743 request); 744 } 745 746 } 747 748 @Override hasStableIds()749 public boolean hasStableIds() { 750 // Whenever bindViewId() is called, the values passed into setId() are stable or 751 // stable-ish. For example, when one contact is modified we don't expect a second 752 // contact's Contact._ID values to change. 753 return true; 754 } 755 bindViewId(final ContactListItemView view, Cursor cursor, int idColumn)756 protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) { 757 // Set a semi-stable id, so that talkback won't get confused when the list gets 758 // refreshed. There is little harm in inserting the same ID twice. 759 long contactId = cursor.getLong(idColumn); 760 view.setId((int) (contactId % Integer.MAX_VALUE)); 761 762 } 763 getContactUri(int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn)764 protected Uri getContactUri(int partitionIndex, Cursor cursor, 765 int contactIdColumn, int lookUpKeyColumn) { 766 long contactId = cursor.getLong(contactIdColumn); 767 String lookupKey = cursor.getString(lookUpKeyColumn); 768 long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId(); 769 Uri uri = Contacts.getLookupUri(contactId, lookupKey); 770 if (uri != null && 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