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