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 ContactEntryListAdapter(Context context)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 */ setFragmentRootView(View fragmentRootView)113 protected void setFragmentRootView(View fragmentRootView) { 114 mFragmentRootView = fragmentRootView; 115 } 116 setDefaultFilterHeaderText(int resourceId)117 protected void setDefaultFilterHeaderText(int resourceId) { 118 mDefaultFilterHeaderText = getContext().getResources().getText(resourceId); 119 } 120 121 @Override newView( Context context, int partition, Cursor cursor, int position, ViewGroup parent)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 bindView(View itemView, int partition, Cursor cursor, int position)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 createPinnedSectionHeaderView(Context context, ViewGroup parent)138 protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) { 139 return new ContactListPinnedHeaderView(context, null, parent); 140 } 141 142 @Override setPinnedSectionTitle(View pinnedHeaderView, String title)143 protected void setPinnedSectionTitle(View pinnedHeaderView, String title) { 144 ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title); 145 } 146 addPartitions()147 protected void addPartitions() { 148 addPartition(createDefaultDirectoryPartition()); 149 } 150 createDefaultDirectoryPartition()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 */ removeDirectoriesAfterDefault()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 getPartitionByDirectoryId(long id)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 getDirectoryById(long id)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 getContactDisplayName(int position)210 public abstract String getContactDisplayName(int position); configureLoader(CursorLoader loader, long directoryId)211 public abstract void configureLoader(CursorLoader loader, long directoryId); 212 213 /** 214 * Marks all partitions as "loading" 215 */ onDataReload()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 clearPartitions()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 isSearchMode()247 public boolean isSearchMode() { 248 return mSearchMode; 249 } 250 setSearchMode(boolean flag)251 public void setSearchMode(boolean flag) { 252 mSearchMode = flag; 253 } 254 getQueryString()255 public String getQueryString() { 256 return mQueryString; 257 } 258 setQueryString(String queryString)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 getUpperCaseQueryString()269 public String getUpperCaseQueryString() { 270 return mUpperCaseQueryString; 271 } 272 getDirectorySearchMode()273 public int getDirectorySearchMode() { 274 return mDirectorySearchMode; 275 } 276 setDirectorySearchMode(int mode)277 public void setDirectorySearchMode(int mode) { 278 mDirectorySearchMode = mode; 279 } 280 getDirectoryResultLimit()281 public int getDirectoryResultLimit() { 282 return mDirectoryResultLimit; 283 } 284 getDirectoryResultLimit(DirectoryPartition directoryPartition)285 public int getDirectoryResultLimit(DirectoryPartition directoryPartition) { 286 final int limit = directoryPartition.getResultLimit(); 287 return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit; 288 } 289 setDirectoryResultLimit(int limit)290 public void setDirectoryResultLimit(int limit) { 291 this.mDirectoryResultLimit = limit; 292 } 293 getContactNameDisplayOrder()294 public int getContactNameDisplayOrder() { 295 return mDisplayOrder; 296 } 297 setContactNameDisplayOrder(int displayOrder)298 public void setContactNameDisplayOrder(int displayOrder) { 299 mDisplayOrder = displayOrder; 300 } 301 getSortOrder()302 public int getSortOrder() { 303 return mSortOrder; 304 } 305 setSortOrder(int sortOrder)306 public void setSortOrder(int sortOrder) { 307 mSortOrder = sortOrder; 308 } 309 setPhotoLoader(ContactPhotoManager photoLoader)310 public void setPhotoLoader(ContactPhotoManager photoLoader) { 311 mPhotoLoader = photoLoader; 312 } 313 getPhotoLoader()314 protected ContactPhotoManager getPhotoLoader() { 315 return mPhotoLoader; 316 } 317 getDisplayPhotos()318 public boolean getDisplayPhotos() { 319 return mDisplayPhotos; 320 } 321 setDisplayPhotos(boolean displayPhotos)322 public void setDisplayPhotos(boolean displayPhotos) { 323 mDisplayPhotos = displayPhotos; 324 } 325 getCircularPhotos()326 public boolean getCircularPhotos() { 327 return mCircularPhotos; 328 } 329 setCircularPhotos(boolean circularPhotos)330 public void setCircularPhotos(boolean circularPhotos) { 331 mCircularPhotos = circularPhotos; 332 } 333 isEmptyListEnabled()334 public boolean isEmptyListEnabled() { 335 return mEmptyListEnabled; 336 } 337 setEmptyListEnabled(boolean flag)338 public void setEmptyListEnabled(boolean flag) { 339 mEmptyListEnabled = flag; 340 } 341 isSelectionVisible()342 public boolean isSelectionVisible() { 343 return mSelectionVisible; 344 } 345 setSelectionVisible(boolean flag)346 public void setSelectionVisible(boolean flag) { 347 this.mSelectionVisible = flag; 348 } 349 isQuickContactEnabled()350 public boolean isQuickContactEnabled() { 351 return mQuickContactEnabled; 352 } 353 setQuickContactEnabled(boolean quickContactEnabled)354 public void setQuickContactEnabled(boolean quickContactEnabled) { 355 mQuickContactEnabled = quickContactEnabled; 356 } 357 isAdjustSelectionBoundsEnabled()358 public boolean isAdjustSelectionBoundsEnabled() { 359 return mAdjustSelectionBoundsEnabled; 360 } 361 setAdjustSelectionBoundsEnabled(boolean enabled)362 public void setAdjustSelectionBoundsEnabled(boolean enabled) { 363 mAdjustSelectionBoundsEnabled = enabled; 364 } 365 shouldIncludeProfile()366 public boolean shouldIncludeProfile() { 367 return mIncludeProfile; 368 } 369 setIncludeProfile(boolean includeProfile)370 public void setIncludeProfile(boolean includeProfile) { 371 mIncludeProfile = includeProfile; 372 } 373 setProfileExists(boolean exists)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 hasProfile()386 public boolean hasProfile() { 387 return mProfileExists; 388 } 389 setDarkTheme(boolean value)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 */ changeDirectories(Cursor cursor)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 changeCursor(int partitionIndex, Cursor cursor)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 changeCursor(Cursor cursor)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 */ updateIndexer(Cursor cursor)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 getExtraStartingSection()526 protected boolean getExtraStartingSection() { 527 return false; 528 } 529 530 @Override getViewTypeCount()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 getItemViewType(int partitionIndex, int position)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 isEmpty()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 isLoading()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 areAllPartitionsEmpty()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 */ configureDefaultPartition(boolean showIfEmpty, boolean hasHeader)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 newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent)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 bindWorkProfileIcon(final ContactListItemView view, int partitionId)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 bindHeaderView(View view, int partitionIndex, Cursor cursor)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. getResultCount(Cursor cursor)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 */ isUserProfile(int position)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 getQuantityText(int count, int zeroResourceId, int pluralResourceId)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 isPhotoSupported(int partitionIndex)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 */ getFilter()719 public ContactListFilter getFilter() { 720 return mFilter; 721 } 722 setFilter(ContactListFilter filter)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 */ bindQuickContact(final ContactListItemView view, int partitionIndex, Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, int lookUpKeyColumn, int displayNameColumn)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 hasStableIds()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 bindViewId(final ContactListItemView view, Cursor cursor, int idColumn)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 getContactUri(int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn)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 */ getDefaultImageRequestFromCursor(Cursor cursor, int displayNameColumn, int lookupKeyColumn)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