BaseRecipientAdapter.java revision 8af0d3b6f34e03c08c8e67be2190da01c59889da
1/* 2 * Copyright (C) 2011 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 17package com.android.ex.chips; 18 19import android.accounts.Account; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.content.pm.PackageManager; 23import android.content.pm.PackageManager.NameNotFoundException; 24import android.content.res.Resources; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.Handler; 28import android.os.Message; 29import android.provider.ContactsContract; 30import android.provider.ContactsContract.Directory; 31import android.text.TextUtils; 32import android.text.util.Rfc822Token; 33import android.util.Log; 34import android.view.View; 35import android.view.ViewGroup; 36import android.widget.AutoCompleteTextView; 37import android.widget.BaseAdapter; 38import android.widget.Filter; 39import android.widget.Filterable; 40 41import com.android.ex.chips.DropdownChipLayouter.AdapterType; 42 43import java.util.ArrayList; 44import java.util.Collections; 45import java.util.HashSet; 46import java.util.LinkedHashMap; 47import java.util.List; 48import java.util.Map; 49import java.util.Set; 50 51/** 52 * Adapter for showing a recipient list. 53 */ 54public class BaseRecipientAdapter extends BaseAdapter implements Filterable, AccountSpecifier, 55 PhotoManager.PhotoManagerCallback { 56 private static final String TAG = "BaseRecipientAdapter"; 57 58 private static final boolean DEBUG = false; 59 60 /** 61 * The preferred number of results to be retrieved. This number may be 62 * exceeded if there are several directories configured, because we will use 63 * the same limit for all directories. 64 */ 65 private static final int DEFAULT_PREFERRED_MAX_RESULT_COUNT = 10; 66 67 /** 68 * The number of extra entries requested to allow for duplicates. Duplicates 69 * are removed from the overall result. 70 */ 71 static final int ALLOWANCE_FOR_DUPLICATES = 5; 72 73 // This is ContactsContract.PRIMARY_ACCOUNT_NAME. Available from ICS as hidden 74 static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account"; 75 // This is ContactsContract.PRIMARY_ACCOUNT_TYPE. Available from ICS as hidden 76 static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account"; 77 78 /** 79 * The "Waiting for more contacts" message will be displayed if search is not complete 80 * within this many milliseconds. 81 */ 82 private static final int MESSAGE_SEARCH_PENDING_DELAY = 1000; 83 /** Used to prepare "Waiting for more contacts" message. */ 84 private static final int MESSAGE_SEARCH_PENDING = 1; 85 86 public static final int QUERY_TYPE_EMAIL = 0; 87 public static final int QUERY_TYPE_PHONE = 1; 88 89 private final Queries.Query mQueryMode; 90 private final int mQueryType; 91 92 /** 93 * Model object for a {@link Directory} row. 94 */ 95 public final static class DirectorySearchParams { 96 public long directoryId; 97 public String directoryType; 98 public String displayName; 99 public String accountName; 100 public String accountType; 101 public CharSequence constraint; 102 public DirectoryFilter filter; 103 } 104 105 protected static class DirectoryListQuery { 106 107 public static final Uri URI = 108 Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories"); 109 public static final String[] PROJECTION = { 110 Directory._ID, // 0 111 Directory.ACCOUNT_NAME, // 1 112 Directory.ACCOUNT_TYPE, // 2 113 Directory.DISPLAY_NAME, // 3 114 Directory.PACKAGE_NAME, // 4 115 Directory.TYPE_RESOURCE_ID, // 5 116 }; 117 118 public static final int ID = 0; 119 public static final int ACCOUNT_NAME = 1; 120 public static final int ACCOUNT_TYPE = 2; 121 public static final int DISPLAY_NAME = 3; 122 public static final int PACKAGE_NAME = 4; 123 public static final int TYPE_RESOURCE_ID = 5; 124 } 125 126 /** Used to temporarily hold results in Cursor objects. */ 127 protected static class TemporaryEntry { 128 public final String displayName; 129 public final String destination; 130 public final int destinationType; 131 public final String destinationLabel; 132 public final long contactId; 133 public final Long directoryId; 134 public final long dataId; 135 public final String thumbnailUriString; 136 public final int displayNameSource; 137 public final String lookupKey; 138 139 public TemporaryEntry( 140 String displayName, 141 String destination, 142 int destinationType, 143 String destinationLabel, 144 long contactId, 145 Long directoryId, 146 long dataId, 147 String thumbnailUriString, 148 int displayNameSource, 149 String lookupKey) { 150 this.displayName = displayName; 151 this.destination = destination; 152 this.destinationType = destinationType; 153 this.destinationLabel = destinationLabel; 154 this.contactId = contactId; 155 this.directoryId = directoryId; 156 this.dataId = dataId; 157 this.thumbnailUriString = thumbnailUriString; 158 this.displayNameSource = displayNameSource; 159 this.lookupKey = lookupKey; 160 } 161 162 public TemporaryEntry(Cursor cursor, Long directoryId) { 163 this.displayName = cursor.getString(Queries.Query.NAME); 164 this.destination = cursor.getString(Queries.Query.DESTINATION); 165 this.destinationType = cursor.getInt(Queries.Query.DESTINATION_TYPE); 166 this.destinationLabel = cursor.getString(Queries.Query.DESTINATION_LABEL); 167 this.contactId = cursor.getLong(Queries.Query.CONTACT_ID); 168 this.directoryId = directoryId; 169 this.dataId = cursor.getLong(Queries.Query.DATA_ID); 170 this.thumbnailUriString = cursor.getString(Queries.Query.PHOTO_THUMBNAIL_URI); 171 this.displayNameSource = cursor.getInt(Queries.Query.DISPLAY_NAME_SOURCE); 172 this.lookupKey = cursor.getString(Queries.Query.LOOKUP_KEY); 173 } 174 } 175 176 /** 177 * Used to pass results from {@link DefaultFilter#performFiltering(CharSequence)} to 178 * {@link DefaultFilter#publishResults(CharSequence, android.widget.Filter.FilterResults)} 179 */ 180 private static class DefaultFilterResult { 181 public final List<RecipientEntry> entries; 182 public final LinkedHashMap<Long, List<RecipientEntry>> entryMap; 183 public final List<RecipientEntry> nonAggregatedEntries; 184 public final Set<String> existingDestinations; 185 public final List<DirectorySearchParams> paramsList; 186 187 public DefaultFilterResult(List<RecipientEntry> entries, 188 LinkedHashMap<Long, List<RecipientEntry>> entryMap, 189 List<RecipientEntry> nonAggregatedEntries, 190 Set<String> existingDestinations, 191 List<DirectorySearchParams> paramsList) { 192 this.entries = entries; 193 this.entryMap = entryMap; 194 this.nonAggregatedEntries = nonAggregatedEntries; 195 this.existingDestinations = existingDestinations; 196 this.paramsList = paramsList; 197 } 198 } 199 200 /** 201 * An asynchronous filter used for loading two data sets: email rows from the local 202 * contact provider and the list of {@link Directory}'s. 203 */ 204 private final class DefaultFilter extends Filter { 205 206 @Override 207 protected FilterResults performFiltering(CharSequence constraint) { 208 if (DEBUG) { 209 Log.d(TAG, "start filtering. constraint: " + constraint + ", thread:" 210 + Thread.currentThread()); 211 } 212 213 final FilterResults results = new FilterResults(); 214 Cursor defaultDirectoryCursor = null; 215 Cursor directoryCursor = null; 216 217 if (TextUtils.isEmpty(constraint)) { 218 clearTempEntries(); 219 // Return empty results. 220 return results; 221 } 222 223 try { 224 defaultDirectoryCursor = doQuery(constraint, mPreferredMaxResultCount, 225 null /* directoryId */); 226 227 if (defaultDirectoryCursor == null) { 228 if (DEBUG) { 229 Log.w(TAG, "null cursor returned for default Email filter query."); 230 } 231 } else { 232 // These variables will become mEntries, mEntryMap, mNonAggregatedEntries, and 233 // mExistingDestinations. Here we shouldn't use those member variables directly 234 // since this method is run outside the UI thread. 235 final LinkedHashMap<Long, List<RecipientEntry>> entryMap = 236 new LinkedHashMap<Long, List<RecipientEntry>>(); 237 final List<RecipientEntry> nonAggregatedEntries = 238 new ArrayList<RecipientEntry>(); 239 final Set<String> existingDestinations = new HashSet<String>(); 240 241 while (defaultDirectoryCursor.moveToNext()) { 242 // Note: At this point each entry doesn't contain any photo 243 // (thus getPhotoBytes() returns null). 244 putOneEntry(new TemporaryEntry(defaultDirectoryCursor, 245 null /* directoryId */), 246 true, entryMap, nonAggregatedEntries, existingDestinations); 247 } 248 249 // We'll copy this result to mEntry in publicResults() (run in the UX thread). 250 final List<RecipientEntry> entries = constructEntryList( 251 entryMap, nonAggregatedEntries); 252 253 final List<DirectorySearchParams> paramsList = 254 searchOtherDirectories(existingDestinations); 255 256 results.values = new DefaultFilterResult( 257 entries, entryMap, nonAggregatedEntries, 258 existingDestinations, paramsList); 259 results.count = 1; 260 } 261 } finally { 262 if (defaultDirectoryCursor != null) { 263 defaultDirectoryCursor.close(); 264 } 265 if (directoryCursor != null) { 266 directoryCursor.close(); 267 } 268 } 269 return results; 270 } 271 272 @Override 273 protected void publishResults(final CharSequence constraint, FilterResults results) { 274 mCurrentConstraint = constraint; 275 276 clearTempEntries(); 277 278 if (results.values != null) { 279 DefaultFilterResult defaultFilterResult = (DefaultFilterResult) results.values; 280 mEntryMap = defaultFilterResult.entryMap; 281 mNonAggregatedEntries = defaultFilterResult.nonAggregatedEntries; 282 mExistingDestinations = defaultFilterResult.existingDestinations; 283 284 // If there are no local results, in the new result set, cache off what had been 285 // shown to the user for use until the first directory result is returned 286 if (defaultFilterResult.entries.size() == 0 && 287 defaultFilterResult.paramsList != null) { 288 cacheCurrentEntries(); 289 } 290 291 updateEntries(defaultFilterResult.entries); 292 293 // We need to search other remote directories, doing other Filter requests. 294 if (defaultFilterResult.paramsList != null) { 295 final int limit = mPreferredMaxResultCount - 296 defaultFilterResult.existingDestinations.size(); 297 startSearchOtherDirectories(constraint, defaultFilterResult.paramsList, limit); 298 } 299 } else { 300 updateEntries(Collections.<RecipientEntry>emptyList()); 301 } 302 } 303 304 @Override 305 public CharSequence convertResultToString(Object resultValue) { 306 final RecipientEntry entry = (RecipientEntry)resultValue; 307 final String displayName = entry.getDisplayName(); 308 final String emailAddress = entry.getDestination(); 309 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { 310 return emailAddress; 311 } else { 312 return new Rfc822Token(displayName, emailAddress, null).toString(); 313 } 314 } 315 } 316 317 protected List<DirectorySearchParams> searchOtherDirectories(Set<String> existingDestinations) { 318 // After having local results, check the size of results. If the results are 319 // not enough, we search remote directories, which will take longer time. 320 final int limit = mPreferredMaxResultCount - existingDestinations.size(); 321 if (limit > 0) { 322 if (DEBUG) { 323 Log.d(TAG, "More entries should be needed (current: " 324 + existingDestinations.size() 325 + ", remaining limit: " + limit + ") "); 326 } 327 final Cursor directoryCursor = mContentResolver.query( 328 DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, 329 null, null, null); 330 return setupOtherDirectories(mContext, directoryCursor, mAccount); 331 } else { 332 // We don't need to search other directories. 333 return null; 334 } 335 } 336 337 /** 338 * An asynchronous filter that performs search in a particular directory. 339 */ 340 protected class DirectoryFilter extends Filter { 341 private final DirectorySearchParams mParams; 342 private int mLimit; 343 344 public DirectoryFilter(DirectorySearchParams params) { 345 mParams = params; 346 } 347 348 public synchronized void setLimit(int limit) { 349 this.mLimit = limit; 350 } 351 352 public synchronized int getLimit() { 353 return this.mLimit; 354 } 355 356 @Override 357 protected FilterResults performFiltering(CharSequence constraint) { 358 if (DEBUG) { 359 Log.d(TAG, "DirectoryFilter#performFiltering. directoryId: " + mParams.directoryId 360 + ", constraint: " + constraint + ", thread: " + Thread.currentThread()); 361 } 362 final FilterResults results = new FilterResults(); 363 results.values = null; 364 results.count = 0; 365 366 if (!TextUtils.isEmpty(constraint)) { 367 final ArrayList<TemporaryEntry> tempEntries = new ArrayList<TemporaryEntry>(); 368 369 Cursor cursor = null; 370 try { 371 // We don't want to pass this Cursor object to UI thread (b/5017608). 372 // Assuming the result should contain fairly small results (at most ~10), 373 // We just copy everything to local structure. 374 cursor = doQuery(constraint, getLimit(), mParams.directoryId); 375 376 if (cursor != null) { 377 while (cursor.moveToNext()) { 378 tempEntries.add(new TemporaryEntry(cursor, mParams.directoryId)); 379 } 380 } 381 } finally { 382 if (cursor != null) { 383 cursor.close(); 384 } 385 } 386 if (!tempEntries.isEmpty()) { 387 results.values = tempEntries; 388 results.count = 1; 389 } 390 } 391 392 if (DEBUG) { 393 Log.v(TAG, "finished loading directory \"" + mParams.displayName + "\"" + 394 " with query " + constraint); 395 } 396 397 return results; 398 } 399 400 @Override 401 protected void publishResults(final CharSequence constraint, FilterResults results) { 402 if (DEBUG) { 403 Log.d(TAG, "DirectoryFilter#publishResult. constraint: " + constraint 404 + ", mCurrentConstraint: " + mCurrentConstraint); 405 } 406 mDelayedMessageHandler.removeDelayedLoadMessage(); 407 // Check if the received result matches the current constraint 408 // If not - the user must have continued typing after the request was issued, which 409 // means several member variables (like mRemainingDirectoryLoad) are already 410 // overwritten so shouldn't be touched here anymore. 411 if (TextUtils.equals(constraint, mCurrentConstraint)) { 412 if (results.count > 0) { 413 @SuppressWarnings("unchecked") 414 final ArrayList<TemporaryEntry> tempEntries = 415 (ArrayList<TemporaryEntry>) results.values; 416 417 for (TemporaryEntry tempEntry : tempEntries) { 418 putOneEntry(tempEntry, mParams.directoryId == Directory.DEFAULT); 419 } 420 } 421 422 // If there are remaining directories, set up delayed message again. 423 mRemainingDirectoryCount--; 424 if (mRemainingDirectoryCount > 0) { 425 if (DEBUG) { 426 Log.d(TAG, "Resend delayed load message. Current mRemainingDirectoryLoad: " 427 + mRemainingDirectoryCount); 428 } 429 mDelayedMessageHandler.sendDelayedLoadMessage(); 430 } 431 432 // If this directory result has some items, or there are no more directories that 433 // we are waiting for, clear the temp results 434 if (results.count > 0 || mRemainingDirectoryCount == 0) { 435 // Clear the temp entries 436 clearTempEntries(); 437 } 438 } 439 440 // Show the list again without "waiting" message. 441 updateEntries(constructEntryList()); 442 } 443 } 444 445 private final Context mContext; 446 private final ContentResolver mContentResolver; 447 private Account mAccount; 448 protected final int mPreferredMaxResultCount; 449 private DropdownChipLayouter mDropdownChipLayouter; 450 451 /** 452 * {@link #mEntries} is responsible for showing every result for this Adapter. To 453 * construct it, we use {@link #mEntryMap}, {@link #mNonAggregatedEntries}, and 454 * {@link #mExistingDestinations}. 455 * 456 * First, each destination (an email address or a phone number) with a valid contactId is 457 * inserted into {@link #mEntryMap} and grouped by the contactId. Destinations without valid 458 * contactId (possible if they aren't in local storage) are stored in 459 * {@link #mNonAggregatedEntries}. 460 * Duplicates are removed using {@link #mExistingDestinations}. 461 * 462 * After having all results from Cursor objects, all destinations in mEntryMap are copied to 463 * {@link #mEntries}. If the number of destinations is not enough (i.e. less than 464 * {@link #mPreferredMaxResultCount}), destinations in mNonAggregatedEntries are also used. 465 * 466 * These variables are only used in UI thread, thus should not be touched in 467 * performFiltering() methods. 468 */ 469 private LinkedHashMap<Long, List<RecipientEntry>> mEntryMap; 470 private List<RecipientEntry> mNonAggregatedEntries; 471 private Set<String> mExistingDestinations; 472 /** Note: use {@link #updateEntries(List)} to update this variable. */ 473 private List<RecipientEntry> mEntries; 474 private List<RecipientEntry> mTempEntries; 475 476 /** The number of directories this adapter is waiting for results. */ 477 private int mRemainingDirectoryCount; 478 479 /** 480 * Used to ignore asynchronous queries with a different constraint, which may happen when 481 * users type characters quickly. 482 */ 483 protected CharSequence mCurrentConstraint; 484 485 /** 486 * Performs all photo querying as well as caching for repeated lookups. 487 */ 488 private PhotoManager mPhotoManager; 489 490 /** 491 * Handler specific for maintaining "Waiting for more contacts" message, which will be shown 492 * when: 493 * - there are directories to be searched 494 * - results from directories are slow to come 495 */ 496 private final class DelayedMessageHandler extends Handler { 497 @Override 498 public void handleMessage(Message msg) { 499 if (mRemainingDirectoryCount > 0) { 500 updateEntries(constructEntryList()); 501 } 502 } 503 504 public void sendDelayedLoadMessage() { 505 sendMessageDelayed(obtainMessage(MESSAGE_SEARCH_PENDING, 0, 0, null), 506 MESSAGE_SEARCH_PENDING_DELAY); 507 } 508 509 public void removeDelayedLoadMessage() { 510 removeMessages(MESSAGE_SEARCH_PENDING); 511 } 512 } 513 514 private final DelayedMessageHandler mDelayedMessageHandler = new DelayedMessageHandler(); 515 516 private EntriesUpdatedObserver mEntriesUpdatedObserver; 517 518 /** 519 * Constructor for email queries. 520 */ 521 public BaseRecipientAdapter(Context context) { 522 this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, QUERY_TYPE_EMAIL); 523 } 524 525 public BaseRecipientAdapter(Context context, int preferredMaxResultCount) { 526 this(context, preferredMaxResultCount, QUERY_TYPE_EMAIL); 527 } 528 529 public BaseRecipientAdapter(int queryMode, Context context) { 530 this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, queryMode); 531 } 532 533 public BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount) { 534 this(context, preferredMaxResultCount, queryMode); 535 } 536 537 public BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode) { 538 mContext = context; 539 mContentResolver = context.getContentResolver(); 540 mPreferredMaxResultCount = preferredMaxResultCount; 541 mPhotoManager = new DefaultPhotoManager(mContentResolver); 542 mQueryType = queryMode; 543 544 if (queryMode == QUERY_TYPE_EMAIL) { 545 mQueryMode = Queries.EMAIL; 546 } else if (queryMode == QUERY_TYPE_PHONE) { 547 mQueryMode = Queries.PHONE; 548 } else { 549 mQueryMode = Queries.EMAIL; 550 Log.e(TAG, "Unsupported query type: " + queryMode); 551 } 552 } 553 554 public Context getContext() { 555 return mContext; 556 } 557 558 public int getQueryType() { 559 return mQueryType; 560 } 561 562 public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) { 563 mDropdownChipLayouter = dropdownChipLayouter; 564 mDropdownChipLayouter.setQuery(mQueryMode); 565 } 566 567 public DropdownChipLayouter getDropdownChipLayouter() { 568 return mDropdownChipLayouter; 569 } 570 571 /** 572 * Enables overriding the default photo manager that is used. 573 */ 574 public void setPhotoManager(PhotoManager photoManager) { 575 mPhotoManager = photoManager; 576 } 577 578 public PhotoManager getPhotoManager() { 579 return mPhotoManager; 580 } 581 582 /** 583 * If true, a null thumbnail uri is ignored when trying to query for a photo. 584 * Derived classes should only return true if a {@link com.android.ex.chips.PhotoManager} 585 * is used that does not rely on thumbnail uris. Default implementation returns {@code false}. 586 * @return 587 */ 588 public boolean ignoreNullThumbnailUri() { 589 return false; 590 } 591 592 /** 593 * If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter} 594 * instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when 595 * clicking on a chip. Default implementation returns {@code false}. 596 */ 597 public boolean forceShowAddress() { 598 return false; 599 } 600 601 /** 602 * Used to replace email addresses with chips. Default behavior 603 * queries the ContactsProvider for contact information about the contact. 604 * Derived classes should override this method if they wish to use a 605 * new data source. 606 * @param inAddresses addresses to query 607 * @param callback callback to return results in case of success or failure 608 */ 609 public void getMatchingRecipients(ArrayList<String> inAddresses, 610 RecipientAlternatesAdapter.RecipientMatchCallback callback) { 611 RecipientAlternatesAdapter.getMatchingRecipients( 612 getContext(), this, inAddresses, getAccount(), callback); 613 } 614 615 /** 616 * Set the account when known. Causes the search to prioritize contacts from that account. 617 */ 618 @Override 619 public void setAccount(Account account) { 620 mAccount = account; 621 } 622 623 /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */ 624 @Override 625 public Filter getFilter() { 626 return new DefaultFilter(); 627 } 628 629 /** 630 * An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows 631 * additional sources of contacts to be considered as matching recipients. 632 * @param addresses A set of addresses to be matched 633 * @return A list of matches or null if none found 634 */ 635 public Map<String, RecipientEntry> getMatchingRecipients(Set<String> addresses) { 636 return null; 637 } 638 639 public static List<DirectorySearchParams> setupOtherDirectories(Context context, 640 Cursor directoryCursor, Account account) { 641 final PackageManager packageManager = context.getPackageManager(); 642 final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>(); 643 DirectorySearchParams preferredDirectory = null; 644 while (directoryCursor.moveToNext()) { 645 final long id = directoryCursor.getLong(DirectoryListQuery.ID); 646 647 // Skip the local invisible directory, because the default directory already includes 648 // all local results. 649 if (id == Directory.LOCAL_INVISIBLE) { 650 continue; 651 } 652 653 final DirectorySearchParams params = new DirectorySearchParams(); 654 final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); 655 final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); 656 params.directoryId = id; 657 params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); 658 params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); 659 params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); 660 if (packageName != null && resourceId != 0) { 661 try { 662 final Resources resources = 663 packageManager.getResourcesForApplication(packageName); 664 params.directoryType = resources.getString(resourceId); 665 if (params.directoryType == null) { 666 Log.e(TAG, "Cannot resolve directory name: " 667 + resourceId + "@" + packageName); 668 } 669 } catch (NameNotFoundException e) { 670 Log.e(TAG, "Cannot resolve directory name: " 671 + resourceId + "@" + packageName, e); 672 } 673 } 674 675 // If an account has been provided and we found a directory that 676 // corresponds to that account, place that directory second, directly 677 // underneath the local contacts. 678 if (account != null && account.name.equals(params.accountName) && 679 account.type.equals(params.accountType)) { 680 preferredDirectory = params; 681 } else { 682 paramsList.add(params); 683 } 684 } 685 686 if (preferredDirectory != null) { 687 paramsList.add(1, preferredDirectory); 688 } 689 690 return paramsList; 691 } 692 693 /** 694 * Starts search in other directories using {@link Filter}. Results will be handled in 695 * {@link DirectoryFilter}. 696 */ 697 protected void startSearchOtherDirectories( 698 CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) { 699 final int count = paramsList.size(); 700 // Note: skipping the default partition (index 0), which has already been loaded 701 for (int i = 1; i < count; i++) { 702 final DirectorySearchParams params = paramsList.get(i); 703 params.constraint = constraint; 704 if (params.filter == null) { 705 params.filter = new DirectoryFilter(params); 706 } 707 params.filter.setLimit(limit); 708 params.filter.filter(constraint); 709 } 710 711 // Directory search started. We may show "waiting" message if directory results are slow 712 // enough. 713 mRemainingDirectoryCount = count - 1; 714 mDelayedMessageHandler.sendDelayedLoadMessage(); 715 } 716 717 /** 718 * Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter} 719 * wants to add an additional entry to the results. Derived classes should override 720 * this method if they are not using the default data structures provided by 721 * {@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their 722 * own data structures to store and collate data. 723 * @param entry the entry being added 724 * @param isAggregatedEntry 725 */ 726 protected void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry) { 727 putOneEntry(entry, isAggregatedEntry, 728 mEntryMap, mNonAggregatedEntries, mExistingDestinations); 729 } 730 731 private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry, 732 LinkedHashMap<Long, List<RecipientEntry>> entryMap, 733 List<RecipientEntry> nonAggregatedEntries, 734 Set<String> existingDestinations) { 735 if (existingDestinations.contains(entry.destination)) { 736 return; 737 } 738 739 existingDestinations.add(entry.destination); 740 741 if (!isAggregatedEntry) { 742 nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry( 743 entry.displayName, 744 entry.displayNameSource, 745 entry.destination, entry.destinationType, entry.destinationLabel, 746 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 747 true, entry.lookupKey)); 748 } else if (entryMap.containsKey(entry.contactId)) { 749 // We already have a section for the person. 750 final List<RecipientEntry> entryList = entryMap.get(entry.contactId); 751 entryList.add(RecipientEntry.constructSecondLevelEntry( 752 entry.displayName, 753 entry.displayNameSource, 754 entry.destination, entry.destinationType, entry.destinationLabel, 755 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 756 true, entry.lookupKey)); 757 } else { 758 final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>(); 759 entryList.add(RecipientEntry.constructTopLevelEntry( 760 entry.displayName, 761 entry.displayNameSource, 762 entry.destination, entry.destinationType, entry.destinationLabel, 763 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 764 true, entry.lookupKey)); 765 entryMap.put(entry.contactId, entryList); 766 } 767 } 768 769 /** 770 * Returns the actual list to use for this Adapter. Derived classes 771 * should override this method if overriding how the adapter stores and collates 772 * data. 773 */ 774 protected List<RecipientEntry> constructEntryList() { 775 return constructEntryList(mEntryMap, mNonAggregatedEntries); 776 } 777 778 /** 779 * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to 780 * fetch a cached photo for each contact entry (other than separators), or request another 781 * thread to get one from directories. 782 */ 783 private List<RecipientEntry> constructEntryList( 784 LinkedHashMap<Long, List<RecipientEntry>> entryMap, 785 List<RecipientEntry> nonAggregatedEntries) { 786 final List<RecipientEntry> entries = new ArrayList<RecipientEntry>(); 787 int validEntryCount = 0; 788 for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) { 789 final List<RecipientEntry> entryList = mapEntry.getValue(); 790 final int size = entryList.size(); 791 for (int i = 0; i < size; i++) { 792 RecipientEntry entry = entryList.get(i); 793 entries.add(entry); 794 mPhotoManager.populatePhotoBytesAsync(entry, this); 795 validEntryCount++; 796 } 797 if (validEntryCount > mPreferredMaxResultCount) { 798 break; 799 } 800 } 801 if (validEntryCount <= mPreferredMaxResultCount) { 802 for (RecipientEntry entry : nonAggregatedEntries) { 803 if (validEntryCount > mPreferredMaxResultCount) { 804 break; 805 } 806 entries.add(entry); 807 mPhotoManager.populatePhotoBytesAsync(entry, this); 808 validEntryCount++; 809 } 810 } 811 812 return entries; 813 } 814 815 816 public interface EntriesUpdatedObserver { 817 public void onChanged(List<RecipientEntry> entries); 818 } 819 820 public void registerUpdateObserver(EntriesUpdatedObserver observer) { 821 mEntriesUpdatedObserver = observer; 822 } 823 824 /** Resets {@link #mEntries} and notify the event to its parent ListView. */ 825 protected void updateEntries(List<RecipientEntry> newEntries) { 826 mEntries = newEntries; 827 mEntriesUpdatedObserver.onChanged(newEntries); 828 notifyDataSetChanged(); 829 } 830 831 protected void cacheCurrentEntries() { 832 mTempEntries = mEntries; 833 } 834 835 protected void clearTempEntries() { 836 mTempEntries = null; 837 } 838 839 protected List<RecipientEntry> getEntries() { 840 return mTempEntries != null ? mTempEntries : mEntries; 841 } 842 843 protected void fetchPhoto(final RecipientEntry entry, final Uri photoThumbnailUri) { 844 mPhotoManager.populatePhotoBytesSync(entry); 845 } 846 847 private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) { 848 final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon() 849 .appendPath(constraint.toString()) 850 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 851 String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); 852 if (directoryId != null) { 853 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 854 String.valueOf(directoryId)); 855 } 856 if (mAccount != null) { 857 builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name); 858 builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type); 859 } 860 final long start = System.currentTimeMillis(); 861 final Cursor cursor = mContentResolver.query( 862 builder.build(), mQueryMode.getProjection(), null, null, null); 863 final long end = System.currentTimeMillis(); 864 if (DEBUG) { 865 Log.d(TAG, "Time for autocomplete (query: " + constraint 866 + ", directoryId: " + directoryId + ", num_of_results: " 867 + (cursor != null ? cursor.getCount() : "null") + "): " 868 + (end - start) + " ms"); 869 } 870 return cursor; 871 } 872 873 // TODO: This won't be used at all. We should find better way to quit the thread.. 874 /*public void close() { 875 mEntries = null; 876 mPhotoCacheMap.evictAll(); 877 if (!sPhotoHandlerThread.quit()) { 878 Log.w(TAG, "Failed to quit photo handler thread, ignoring it."); 879 } 880 }*/ 881 882 @Override 883 public int getCount() { 884 final List<RecipientEntry> entries = getEntries(); 885 return entries != null ? entries.size() : 0; 886 } 887 888 @Override 889 public RecipientEntry getItem(int position) { 890 return getEntries().get(position); 891 } 892 893 @Override 894 public long getItemId(int position) { 895 return position; 896 } 897 898 @Override 899 public int getViewTypeCount() { 900 return RecipientEntry.ENTRY_TYPE_SIZE; 901 } 902 903 @Override 904 public int getItemViewType(int position) { 905 return getEntries().get(position).getEntryType(); 906 } 907 908 @Override 909 public boolean isEnabled(int position) { 910 return getEntries().get(position).isSelectable(); 911 } 912 913 @Override 914 public View getView(int position, View convertView, ViewGroup parent) { 915 final RecipientEntry entry = getEntries().get(position); 916 917 final String constraint = mCurrentConstraint == null ? null : 918 mCurrentConstraint.toString(); 919 920 return mDropdownChipLayouter.bindView(convertView, parent, entry, position, 921 AdapterType.BASE_RECIPIENT, constraint); 922 } 923 924 public Account getAccount() { 925 return mAccount; 926 } 927 928 @Override 929 public void onPhotoBytesAsynchronouslyPopulated() { 930 notifyDataSetChanged(); 931 } 932} 933