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