ContactEntryListAdapter.java revision bd80fd64b9ff94c9ffbdb843beb4b363bb209463
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 */ 16package com.android.contacts.common.list; 17 18import android.content.Context; 19import android.content.CursorLoader; 20import android.database.Cursor; 21import android.net.Uri; 22import android.os.Bundle; 23import android.provider.ContactsContract; 24import android.provider.ContactsContract.ContactCounts; 25import android.provider.ContactsContract.Contacts; 26import android.provider.ContactsContract.Directory; 27import android.text.TextUtils; 28import android.util.Log; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.ViewGroup; 32import android.widget.QuickContactBadge; 33import android.widget.SectionIndexer; 34import android.widget.TextView; 35 36import com.android.contacts.common.ContactPhotoManager; 37import com.android.contacts.common.R; 38 39import java.util.HashSet; 40 41/** 42 * Common base class for various contact-related lists, e.g. contact list, phone number list 43 * etc. 44 */ 45public abstract class ContactEntryListAdapter extends IndexerListAdapter { 46 47 private static final String TAG = "ContactEntryListAdapter"; 48 49 /** 50 * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should 51 * be included in the search. 52 */ 53 public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false; 54 55 private int mDisplayOrder; 56 private int mSortOrder; 57 58 private boolean mDisplayPhotos; 59 private boolean mQuickContactEnabled; 60 61 /** 62 * indicates if contact queries include profile 63 */ 64 private boolean mIncludeProfile; 65 66 /** 67 * indicates if query results includes a profile 68 */ 69 private boolean mProfileExists; 70 71 private ContactPhotoManager mPhotoLoader; 72 73 private String mQueryString; 74 private String mUpperCaseQueryString; 75 private boolean mSearchMode; 76 private int mDirectorySearchMode; 77 private int mDirectoryResultLimit = Integer.MAX_VALUE; 78 79 private boolean mEmptyListEnabled = true; 80 81 private boolean mSelectionVisible; 82 83 private ContactListFilter mFilter; 84 private String mContactsCount = ""; 85 private boolean mDarkTheme = false; 86 87 /** Resource used to provide header-text for default filter. */ 88 private CharSequence mDefaultFilterHeaderText; 89 90 public ContactEntryListAdapter(Context context) { 91 super(context); 92 addPartitions(); 93 setDefaultFilterHeaderText(R.string.local_search_label); 94 } 95 96 protected void setDefaultFilterHeaderText(int resourceId) { 97 mDefaultFilterHeaderText = getContext().getResources().getText(resourceId); 98 } 99 100 @Override 101 protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) { 102 return new ContactListPinnedHeaderView(context, null); 103 } 104 105 @Override 106 protected void setPinnedSectionTitle(View pinnedHeaderView, String title) { 107 ((ContactListPinnedHeaderView)pinnedHeaderView).setSectionHeader(title); 108 } 109 110 @Override 111 protected void setPinnedHeaderContactsCount(View header) { 112 // Update the header with the contacts count only if a profile header exists 113 // otherwise, the contacts count are shown in the empty profile header view 114 if (mProfileExists) { 115 ((ContactListPinnedHeaderView)header).setCountView(mContactsCount); 116 } else { 117 clearPinnedHeaderContactsCount(header); 118 } 119 } 120 121 @Override 122 protected void clearPinnedHeaderContactsCount(View header) { 123 ((ContactListPinnedHeaderView)header).setCountView(null); 124 } 125 126 protected void addPartitions() { 127 addPartition(createDefaultDirectoryPartition()); 128 } 129 130 protected DirectoryPartition createDefaultDirectoryPartition() { 131 DirectoryPartition partition = new DirectoryPartition(true, true); 132 partition.setDirectoryId(Directory.DEFAULT); 133 partition.setDirectoryType(getContext().getString(R.string.contactsList)); 134 partition.setPriorityDirectory(true); 135 partition.setPhotoSupported(true); 136 return partition; 137 } 138 139 /** 140 * Remove all directories after the default directory. This is typically used when contacts 141 * list screens are asked to exit the search mode and thus need to remove all remote directory 142 * results for the search. 143 * 144 * This code assumes that the default directory and directories before that should not be 145 * deleted (e.g. Join screen has "suggested contacts" directory before the default director, 146 * and we should not remove the directory). 147 */ 148 public void removeDirectoriesAfterDefault() { 149 final int partitionCount = getPartitionCount(); 150 for (int i = partitionCount - 1; i >= 0; i--) { 151 final Partition partition = getPartition(i); 152 if ((partition instanceof DirectoryPartition) 153 && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) { 154 break; 155 } else { 156 removePartition(i); 157 } 158 } 159 } 160 161 private int getPartitionByDirectoryId(long id) { 162 int count = getPartitionCount(); 163 for (int i = 0; i < count; i++) { 164 Partition partition = getPartition(i); 165 if (partition instanceof DirectoryPartition) { 166 if (((DirectoryPartition)partition).getDirectoryId() == id) { 167 return i; 168 } 169 } 170 } 171 return -1; 172 } 173 174 public abstract String getContactDisplayName(int position); 175 public abstract void configureLoader(CursorLoader loader, long directoryId); 176 177 /** 178 * Marks all partitions as "loading" 179 */ 180 public void onDataReload() { 181 boolean notify = false; 182 int count = getPartitionCount(); 183 for (int i = 0; i < count; i++) { 184 Partition partition = getPartition(i); 185 if (partition instanceof DirectoryPartition) { 186 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 187 if (!directoryPartition.isLoading()) { 188 notify = true; 189 } 190 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); 191 } 192 } 193 if (notify) { 194 notifyDataSetChanged(); 195 } 196 } 197 198 @Override 199 public void clearPartitions() { 200 int count = getPartitionCount(); 201 for (int i = 0; i < count; i++) { 202 Partition partition = getPartition(i); 203 if (partition instanceof DirectoryPartition) { 204 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 205 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); 206 } 207 } 208 super.clearPartitions(); 209 } 210 211 public boolean isSearchMode() { 212 return mSearchMode; 213 } 214 215 public void setSearchMode(boolean flag) { 216 mSearchMode = flag; 217 } 218 219 public String getQueryString() { 220 return mQueryString; 221 } 222 223 public void setQueryString(String queryString) { 224 mQueryString = queryString; 225 if (TextUtils.isEmpty(queryString)) { 226 mUpperCaseQueryString = null; 227 } else { 228 mUpperCaseQueryString = queryString.toUpperCase(); 229 } 230 } 231 232 public String getUpperCaseQueryString() { 233 return mUpperCaseQueryString; 234 } 235 236 public int getDirectorySearchMode() { 237 return mDirectorySearchMode; 238 } 239 240 public void setDirectorySearchMode(int mode) { 241 mDirectorySearchMode = mode; 242 } 243 244 public int getDirectoryResultLimit() { 245 return mDirectoryResultLimit; 246 } 247 248 public void setDirectoryResultLimit(int limit) { 249 this.mDirectoryResultLimit = limit; 250 } 251 252 public int getContactNameDisplayOrder() { 253 return mDisplayOrder; 254 } 255 256 public void setContactNameDisplayOrder(int displayOrder) { 257 mDisplayOrder = displayOrder; 258 } 259 260 public int getSortOrder() { 261 return mSortOrder; 262 } 263 264 public void setSortOrder(int sortOrder) { 265 mSortOrder = sortOrder; 266 } 267 268 public void setPhotoLoader(ContactPhotoManager photoLoader) { 269 mPhotoLoader = photoLoader; 270 } 271 272 protected ContactPhotoManager getPhotoLoader() { 273 return mPhotoLoader; 274 } 275 276 public boolean getDisplayPhotos() { 277 return mDisplayPhotos; 278 } 279 280 public void setDisplayPhotos(boolean displayPhotos) { 281 mDisplayPhotos = displayPhotos; 282 } 283 284 public boolean isEmptyListEnabled() { 285 return mEmptyListEnabled; 286 } 287 288 public void setEmptyListEnabled(boolean flag) { 289 mEmptyListEnabled = flag; 290 } 291 292 public boolean isSelectionVisible() { 293 return mSelectionVisible; 294 } 295 296 public void setSelectionVisible(boolean flag) { 297 this.mSelectionVisible = flag; 298 } 299 300 public boolean isQuickContactEnabled() { 301 return mQuickContactEnabled; 302 } 303 304 public void setQuickContactEnabled(boolean quickContactEnabled) { 305 mQuickContactEnabled = quickContactEnabled; 306 } 307 308 public boolean shouldIncludeProfile() { 309 return mIncludeProfile; 310 } 311 312 public void setIncludeProfile(boolean includeProfile) { 313 mIncludeProfile = includeProfile; 314 } 315 316 public void setProfileExists(boolean exists) { 317 mProfileExists = exists; 318 // Stick the "ME" header for the profile 319 if (exists) { 320 SectionIndexer indexer = getIndexer(); 321 if (indexer != null) { 322 ((ContactsSectionIndexer) indexer).setProfileHeader( 323 getContext().getString(R.string.user_profile_contacts_list_header)); 324 } 325 } 326 } 327 328 public boolean hasProfile() { 329 return mProfileExists; 330 } 331 332 public void setDarkTheme(boolean value) { 333 mDarkTheme = value; 334 } 335 336 /** 337 * Updates partitions according to the directory meta-data contained in the supplied 338 * cursor. 339 */ 340 public void changeDirectories(Cursor cursor) { 341 if (cursor.getCount() == 0) { 342 // Directory table must have at least local directory, without which this adapter will 343 // enter very weird state. 344 Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " + 345 "no directory entries.", new RuntimeException()); 346 return; 347 } 348 HashSet<Long> directoryIds = new HashSet<Long>(); 349 350 int idColumnIndex = cursor.getColumnIndex(Directory._ID); 351 int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE); 352 int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME); 353 int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT); 354 355 // TODO preserve the order of partition to match those of the cursor 356 // Phase I: add new directories 357 cursor.moveToPosition(-1); 358 while (cursor.moveToNext()) { 359 long id = cursor.getLong(idColumnIndex); 360 directoryIds.add(id); 361 if (getPartitionByDirectoryId(id) == -1) { 362 DirectoryPartition partition = new DirectoryPartition(false, true); 363 partition.setDirectoryId(id); 364 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex)); 365 partition.setDisplayName(cursor.getString(displayNameColumnIndex)); 366 int photoSupport = cursor.getInt(photoSupportColumnIndex); 367 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY 368 || photoSupport == Directory.PHOTO_SUPPORT_FULL); 369 addPartition(partition); 370 } 371 } 372 373 // Phase II: remove deleted directories 374 int count = getPartitionCount(); 375 for (int i = count; --i >= 0; ) { 376 Partition partition = getPartition(i); 377 if (partition instanceof DirectoryPartition) { 378 long id = ((DirectoryPartition)partition).getDirectoryId(); 379 if (!directoryIds.contains(id)) { 380 removePartition(i); 381 } 382 } 383 } 384 385 invalidate(); 386 notifyDataSetChanged(); 387 } 388 389 @Override 390 public void changeCursor(int partitionIndex, Cursor cursor) { 391 if (partitionIndex >= getPartitionCount()) { 392 // There is no partition for this data 393 return; 394 } 395 396 Partition partition = getPartition(partitionIndex); 397 if (partition instanceof DirectoryPartition) { 398 ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED); 399 } 400 401 if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) { 402 mPhotoLoader.refreshCache(); 403 } 404 405 super.changeCursor(partitionIndex, cursor); 406 407 if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { 408 updateIndexer(cursor); 409 } 410 } 411 412 public void changeCursor(Cursor cursor) { 413 changeCursor(0, cursor); 414 } 415 416 /** 417 * Updates the indexer, which is used to produce section headers. 418 */ 419 private void updateIndexer(Cursor cursor) { 420 if (cursor == null) { 421 setIndexer(null); 422 return; 423 } 424 425 Bundle bundle = cursor.getExtras(); 426 if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) { 427 String sections[] = 428 bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); 429 int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); 430 setIndexer(new ContactsSectionIndexer(sections, counts)); 431 } else { 432 setIndexer(null); 433 } 434 } 435 436 @Override 437 public int getViewTypeCount() { 438 // We need a separate view type for each item type, plus another one for 439 // each type with header, plus one for "other". 440 return getItemViewTypeCount() * 2 + 1; 441 } 442 443 @Override 444 public int getItemViewType(int partitionIndex, int position) { 445 int type = super.getItemViewType(partitionIndex, position); 446 if (!isUserProfile(position) 447 && isSectionHeaderDisplayEnabled() 448 && partitionIndex == getIndexedPartition()) { 449 Placement placement = getItemPlacementInSection(position); 450 return placement.firstInSection ? type : getItemViewTypeCount() + type; 451 } else { 452 return type; 453 } 454 } 455 456 @Override 457 public boolean isEmpty() { 458 // TODO 459// if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) { 460// return true; 461// } 462 463 if (!mEmptyListEnabled) { 464 return false; 465 } else if (isSearchMode()) { 466 return TextUtils.isEmpty(getQueryString()); 467 } else { 468 return super.isEmpty(); 469 } 470 } 471 472 public boolean isLoading() { 473 int count = getPartitionCount(); 474 for (int i = 0; i < count; i++) { 475 Partition partition = getPartition(i); 476 if (partition instanceof DirectoryPartition 477 && ((DirectoryPartition) partition).isLoading()) { 478 return true; 479 } 480 } 481 return false; 482 } 483 484 public boolean areAllPartitionsEmpty() { 485 int count = getPartitionCount(); 486 for (int i = 0; i < count; i++) { 487 if (!isPartitionEmpty(i)) { 488 return false; 489 } 490 } 491 return true; 492 } 493 494 /** 495 * Changes visibility parameters for the default directory partition. 496 */ 497 public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) { 498 int defaultPartitionIndex = -1; 499 int count = getPartitionCount(); 500 for (int i = 0; i < count; i++) { 501 Partition partition = getPartition(i); 502 if (partition instanceof DirectoryPartition && 503 ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) { 504 defaultPartitionIndex = i; 505 break; 506 } 507 } 508 if (defaultPartitionIndex != -1) { 509 setShowIfEmpty(defaultPartitionIndex, showIfEmpty); 510 setHasHeader(defaultPartitionIndex, hasHeader); 511 } 512 } 513 514 @Override 515 protected View newHeaderView(Context context, int partition, Cursor cursor, 516 ViewGroup parent) { 517 LayoutInflater inflater = LayoutInflater.from(context); 518 return inflater.inflate(R.layout.directory_header, parent, false); 519 } 520 521 @Override 522 protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) { 523 Partition partition = getPartition(partitionIndex); 524 if (!(partition instanceof DirectoryPartition)) { 525 return; 526 } 527 528 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 529 long directoryId = directoryPartition.getDirectoryId(); 530 TextView labelTextView = (TextView)view.findViewById(R.id.label); 531 TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name); 532 if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { 533 labelTextView.setText(mDefaultFilterHeaderText); 534 displayNameTextView.setText(null); 535 } else { 536 labelTextView.setText(R.string.directory_search_label); 537 String directoryName = directoryPartition.getDisplayName(); 538 String displayName = !TextUtils.isEmpty(directoryName) 539 ? directoryName 540 : directoryPartition.getDirectoryType(); 541 displayNameTextView.setText(displayName); 542 } 543 544 TextView countText = (TextView)view.findViewById(R.id.count); 545 if (directoryPartition.isLoading()) { 546 countText.setText(R.string.search_results_searching); 547 } else { 548 int count = cursor == null ? 0 : cursor.getCount(); 549 if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE 550 && count >= getDirectoryResultLimit()) { 551 countText.setText(mContext.getString( 552 R.string.foundTooManyContacts, getDirectoryResultLimit())); 553 } else { 554 countText.setText(getQuantityText( 555 count, R.string.listFoundAllContactsZero, R.plurals.searchFoundContacts)); 556 } 557 } 558 } 559 560 /** 561 * Checks whether the contact entry at the given position represents the user's profile. 562 */ 563 protected boolean isUserProfile(int position) { 564 // The profile only ever appears in the first position if it is present. So if the position 565 // is anything beyond 0, it can't be the profile. 566 boolean isUserProfile = false; 567 if (position == 0) { 568 int partition = getPartitionForPosition(position); 569 if (partition >= 0) { 570 // Save the old cursor position - the call to getItem() may modify the cursor 571 // position. 572 int offset = getCursor(partition).getPosition(); 573 Cursor cursor = (Cursor) getItem(position); 574 if (cursor != null) { 575 int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE); 576 if (profileColumnIndex != -1) { 577 isUserProfile = cursor.getInt(profileColumnIndex) == 1; 578 } 579 // Restore the old cursor position. 580 cursor.moveToPosition(offset); 581 } 582 } 583 } 584 return isUserProfile; 585 } 586 587 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly 588 public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { 589 if (count == 0) { 590 return getContext().getString(zeroResourceId); 591 } else { 592 String format = getContext().getResources() 593 .getQuantityText(pluralResourceId, count).toString(); 594 return String.format(format, count); 595 } 596 } 597 598 public boolean isPhotoSupported(int partitionIndex) { 599 Partition partition = getPartition(partitionIndex); 600 if (partition instanceof DirectoryPartition) { 601 return ((DirectoryPartition) partition).isPhotoSupported(); 602 } 603 return true; 604 } 605 606 /** 607 * Returns the currently selected filter. 608 */ 609 public ContactListFilter getFilter() { 610 return mFilter; 611 } 612 613 public void setFilter(ContactListFilter filter) { 614 mFilter = filter; 615 } 616 617 // TODO: move sharable logic (bindXX() methods) to here with extra arguments 618 619 /** 620 * Loads the photo for the quick contact view and assigns the contact uri. 621 * @param photoIdColumn Index of the photo id column 622 * @param photoUriColumn Index of the photo uri column. Optional: Can be -1 623 * @param contactIdColumn Index of the contact id column 624 * @param lookUpKeyColumn Index of the lookup key column 625 */ 626 protected void bindQuickContact(final ContactListItemView view, int partitionIndex, 627 Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn, 628 int lookUpKeyColumn) { 629 long photoId = 0; 630 if (!cursor.isNull(photoIdColumn)) { 631 photoId = cursor.getLong(photoIdColumn); 632 } 633 634 QuickContactBadge quickContact = view.getQuickContact(); 635 quickContact.assignContactUri( 636 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn)); 637 638 if (photoId != 0 || photoUriColumn == -1) { 639 getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme); 640 } else { 641 final String photoUriString = cursor.getString(photoUriColumn); 642 final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); 643 getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme); 644 } 645 646 } 647 648 protected Uri getContactUri(int partitionIndex, Cursor cursor, 649 int contactIdColumn, int lookUpKeyColumn) { 650 long contactId = cursor.getLong(contactIdColumn); 651 String lookupKey = cursor.getString(lookUpKeyColumn); 652 Uri uri = Contacts.getLookupUri(contactId, lookupKey); 653 long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId(); 654 if (directoryId != Directory.DEFAULT) { 655 uri = uri.buildUpon().appendQueryParameter( 656 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build(); 657 } 658 return uri; 659 } 660 661 public void setContactsCount(String count) { 662 mContactsCount = count; 663 } 664 665 public String getContactsCount() { 666 return mContactsCount; 667 } 668} 669