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 17 package com.android.ex.chips; 18 19 import android.accounts.Account; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.pm.PackageManager; 23 import android.content.pm.PackageManager.NameNotFoundException; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.Handler; 28 import android.os.Message; 29 import android.provider.ContactsContract; 30 import android.provider.ContactsContract.Directory; 31 import android.text.TextUtils; 32 import android.text.util.Rfc822Token; 33 import android.util.Log; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.AutoCompleteTextView; 37 import android.widget.BaseAdapter; 38 import android.widget.Filter; 39 import android.widget.Filterable; 40 41 import com.android.ex.chips.DropdownChipLayouter.AdapterType; 42 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.HashSet; 46 import java.util.LinkedHashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 51 /** 52 * Adapter for showing a recipient list. 53 */ 54 public 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 TemporaryEntry( String displayName, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, String thumbnailUriString, int displayNameSource, String lookupKey)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 TemporaryEntry(Cursor cursor, Long directoryId)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 DefaultFilterResult(List<RecipientEntry> entries, LinkedHashMap<Long, List<RecipientEntry>> entryMap, List<RecipientEntry> nonAggregatedEntries, Set<String> existingDestinations, List<DirectorySearchParams> paramsList)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 performFiltering(CharSequence constraint)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 publishResults(final CharSequence constraint, FilterResults results)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 convertResultToString(Object resultValue)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 searchOtherDirectories(Set<String> existingDestinations)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 DirectoryFilter(DirectorySearchParams params)344 public DirectoryFilter(DirectorySearchParams params) { 345 mParams = params; 346 } 347 setLimit(int limit)348 public synchronized void setLimit(int limit) { 349 this.mLimit = limit; 350 } 351 getLimit()352 public synchronized int getLimit() { 353 return this.mLimit; 354 } 355 356 @Override performFiltering(CharSequence constraint)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 publishResults(final CharSequence constraint, FilterResults results)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 handleMessage(Message msg)498 public void handleMessage(Message msg) { 499 if (mRemainingDirectoryCount > 0) { 500 updateEntries(constructEntryList()); 501 } 502 } 503 sendDelayedLoadMessage()504 public void sendDelayedLoadMessage() { 505 sendMessageDelayed(obtainMessage(MESSAGE_SEARCH_PENDING, 0, 0, null), 506 MESSAGE_SEARCH_PENDING_DELAY); 507 } 508 removeDelayedLoadMessage()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 */ BaseRecipientAdapter(Context context)521 public BaseRecipientAdapter(Context context) { 522 this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, QUERY_TYPE_EMAIL); 523 } 524 BaseRecipientAdapter(Context context, int preferredMaxResultCount)525 public BaseRecipientAdapter(Context context, int preferredMaxResultCount) { 526 this(context, preferredMaxResultCount, QUERY_TYPE_EMAIL); 527 } 528 BaseRecipientAdapter(int queryMode, Context context)529 public BaseRecipientAdapter(int queryMode, Context context) { 530 this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, queryMode); 531 } 532 BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount)533 public BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount) { 534 this(context, preferredMaxResultCount, queryMode); 535 } 536 BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode)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 getContext()554 public Context getContext() { 555 return mContext; 556 } 557 getQueryType()558 public int getQueryType() { 559 return mQueryType; 560 } 561 setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter)562 public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) { 563 mDropdownChipLayouter = dropdownChipLayouter; 564 mDropdownChipLayouter.setQuery(mQueryMode); 565 } 566 getDropdownChipLayouter()567 public DropdownChipLayouter getDropdownChipLayouter() { 568 return mDropdownChipLayouter; 569 } 570 571 /** 572 * Enables overriding the default photo manager that is used. 573 */ setPhotoManager(PhotoManager photoManager)574 public void setPhotoManager(PhotoManager photoManager) { 575 mPhotoManager = photoManager; 576 } 577 getPhotoManager()578 public PhotoManager getPhotoManager() { 579 return mPhotoManager; 580 } 581 582 /** 583 * If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter} 584 * instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when 585 * clicking on a chip. Default implementation returns {@code false}. 586 */ forceShowAddress()587 public boolean forceShowAddress() { 588 return false; 589 } 590 591 /** 592 * Used to replace email addresses with chips. Default behavior 593 * queries the ContactsProvider for contact information about the contact. 594 * Derived classes should override this method if they wish to use a 595 * new data source. 596 * @param inAddresses addresses to query 597 * @param callback callback to return results in case of success or failure 598 */ getMatchingRecipients(ArrayList<String> inAddresses, RecipientAlternatesAdapter.RecipientMatchCallback callback)599 public void getMatchingRecipients(ArrayList<String> inAddresses, 600 RecipientAlternatesAdapter.RecipientMatchCallback callback) { 601 RecipientAlternatesAdapter.getMatchingRecipients( 602 getContext(), this, inAddresses, getAccount(), callback); 603 } 604 605 /** 606 * Set the account when known. Causes the search to prioritize contacts from that account. 607 */ 608 @Override setAccount(Account account)609 public void setAccount(Account account) { 610 mAccount = account; 611 } 612 613 /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */ 614 @Override getFilter()615 public Filter getFilter() { 616 return new DefaultFilter(); 617 } 618 619 /** 620 * An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows 621 * additional sources of contacts to be considered as matching recipients. 622 * @param addresses A set of addresses to be matched 623 * @return A list of matches or null if none found 624 */ getMatchingRecipients(Set<String> addresses)625 public Map<String, RecipientEntry> getMatchingRecipients(Set<String> addresses) { 626 return null; 627 } 628 setupOtherDirectories(Context context, Cursor directoryCursor, Account account)629 public static List<DirectorySearchParams> setupOtherDirectories(Context context, 630 Cursor directoryCursor, Account account) { 631 final PackageManager packageManager = context.getPackageManager(); 632 final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>(); 633 DirectorySearchParams preferredDirectory = null; 634 while (directoryCursor.moveToNext()) { 635 final long id = directoryCursor.getLong(DirectoryListQuery.ID); 636 637 // Skip the local invisible directory, because the default directory already includes 638 // all local results. 639 if (id == Directory.LOCAL_INVISIBLE) { 640 continue; 641 } 642 643 final DirectorySearchParams params = new DirectorySearchParams(); 644 final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); 645 final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); 646 params.directoryId = id; 647 params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); 648 params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); 649 params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); 650 if (packageName != null && resourceId != 0) { 651 try { 652 final Resources resources = 653 packageManager.getResourcesForApplication(packageName); 654 params.directoryType = resources.getString(resourceId); 655 if (params.directoryType == null) { 656 Log.e(TAG, "Cannot resolve directory name: " 657 + resourceId + "@" + packageName); 658 } 659 } catch (NameNotFoundException e) { 660 Log.e(TAG, "Cannot resolve directory name: " 661 + resourceId + "@" + packageName, e); 662 } 663 } 664 665 // If an account has been provided and we found a directory that 666 // corresponds to that account, place that directory second, directly 667 // underneath the local contacts. 668 if (account != null && account.name.equals(params.accountName) && 669 account.type.equals(params.accountType)) { 670 preferredDirectory = params; 671 } else { 672 paramsList.add(params); 673 } 674 } 675 676 if (preferredDirectory != null) { 677 paramsList.add(1, preferredDirectory); 678 } 679 680 return paramsList; 681 } 682 683 /** 684 * Starts search in other directories using {@link Filter}. Results will be handled in 685 * {@link DirectoryFilter}. 686 */ startSearchOtherDirectories( CharSequence constraint, List<DirectorySearchParams> paramsList, int limit)687 protected void startSearchOtherDirectories( 688 CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) { 689 final int count = paramsList.size(); 690 // Note: skipping the default partition (index 0), which has already been loaded 691 for (int i = 1; i < count; i++) { 692 final DirectorySearchParams params = paramsList.get(i); 693 params.constraint = constraint; 694 if (params.filter == null) { 695 params.filter = new DirectoryFilter(params); 696 } 697 params.filter.setLimit(limit); 698 params.filter.filter(constraint); 699 } 700 701 // Directory search started. We may show "waiting" message if directory results are slow 702 // enough. 703 mRemainingDirectoryCount = count - 1; 704 mDelayedMessageHandler.sendDelayedLoadMessage(); 705 } 706 707 /** 708 * Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter} 709 * wants to add an additional entry to the results. Derived classes should override 710 * this method if they are not using the default data structures provided by 711 * {@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their 712 * own data structures to store and collate data. 713 * @param entry the entry being added 714 * @param isAggregatedEntry 715 */ putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry)716 protected void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry) { 717 putOneEntry(entry, isAggregatedEntry, 718 mEntryMap, mNonAggregatedEntries, mExistingDestinations); 719 } 720 putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry, LinkedHashMap<Long, List<RecipientEntry>> entryMap, List<RecipientEntry> nonAggregatedEntries, Set<String> existingDestinations)721 private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry, 722 LinkedHashMap<Long, List<RecipientEntry>> entryMap, 723 List<RecipientEntry> nonAggregatedEntries, 724 Set<String> existingDestinations) { 725 if (existingDestinations.contains(entry.destination)) { 726 return; 727 } 728 729 existingDestinations.add(entry.destination); 730 731 if (!isAggregatedEntry) { 732 nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry( 733 entry.displayName, 734 entry.displayNameSource, 735 entry.destination, entry.destinationType, entry.destinationLabel, 736 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 737 true, entry.lookupKey)); 738 } else if (entryMap.containsKey(entry.contactId)) { 739 // We already have a section for the person. 740 final List<RecipientEntry> entryList = entryMap.get(entry.contactId); 741 entryList.add(RecipientEntry.constructSecondLevelEntry( 742 entry.displayName, 743 entry.displayNameSource, 744 entry.destination, entry.destinationType, entry.destinationLabel, 745 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 746 true, entry.lookupKey)); 747 } else { 748 final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>(); 749 entryList.add(RecipientEntry.constructTopLevelEntry( 750 entry.displayName, 751 entry.displayNameSource, 752 entry.destination, entry.destinationType, entry.destinationLabel, 753 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 754 true, entry.lookupKey)); 755 entryMap.put(entry.contactId, entryList); 756 } 757 } 758 759 /** 760 * Returns the actual list to use for this Adapter. Derived classes 761 * should override this method if overriding how the adapter stores and collates 762 * data. 763 */ constructEntryList()764 protected List<RecipientEntry> constructEntryList() { 765 return constructEntryList(mEntryMap, mNonAggregatedEntries); 766 } 767 768 /** 769 * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to 770 * fetch a cached photo for each contact entry (other than separators), or request another 771 * thread to get one from directories. 772 */ constructEntryList( LinkedHashMap<Long, List<RecipientEntry>> entryMap, List<RecipientEntry> nonAggregatedEntries)773 private List<RecipientEntry> constructEntryList( 774 LinkedHashMap<Long, List<RecipientEntry>> entryMap, 775 List<RecipientEntry> nonAggregatedEntries) { 776 final List<RecipientEntry> entries = new ArrayList<RecipientEntry>(); 777 int validEntryCount = 0; 778 for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) { 779 final List<RecipientEntry> entryList = mapEntry.getValue(); 780 final int size = entryList.size(); 781 for (int i = 0; i < size; i++) { 782 RecipientEntry entry = entryList.get(i); 783 entries.add(entry); 784 mPhotoManager.populatePhotoBytesAsync(entry, this); 785 validEntryCount++; 786 } 787 if (validEntryCount > mPreferredMaxResultCount) { 788 break; 789 } 790 } 791 if (validEntryCount <= mPreferredMaxResultCount) { 792 for (RecipientEntry entry : nonAggregatedEntries) { 793 if (validEntryCount > mPreferredMaxResultCount) { 794 break; 795 } 796 entries.add(entry); 797 mPhotoManager.populatePhotoBytesAsync(entry, this); 798 validEntryCount++; 799 } 800 } 801 802 return entries; 803 } 804 805 806 public interface EntriesUpdatedObserver { onChanged(List<RecipientEntry> entries)807 public void onChanged(List<RecipientEntry> entries); 808 } 809 registerUpdateObserver(EntriesUpdatedObserver observer)810 public void registerUpdateObserver(EntriesUpdatedObserver observer) { 811 mEntriesUpdatedObserver = observer; 812 } 813 814 /** Resets {@link #mEntries} and notify the event to its parent ListView. */ updateEntries(List<RecipientEntry> newEntries)815 protected void updateEntries(List<RecipientEntry> newEntries) { 816 mEntries = newEntries; 817 mEntriesUpdatedObserver.onChanged(newEntries); 818 notifyDataSetChanged(); 819 } 820 cacheCurrentEntries()821 protected void cacheCurrentEntries() { 822 mTempEntries = mEntries; 823 } 824 clearTempEntries()825 protected void clearTempEntries() { 826 mTempEntries = null; 827 } 828 getEntries()829 protected List<RecipientEntry> getEntries() { 830 return mTempEntries != null ? mTempEntries : mEntries; 831 } 832 fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb)833 protected void fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb) { 834 mPhotoManager.populatePhotoBytesAsync(entry, cb); 835 } 836 doQuery(CharSequence constraint, int limit, Long directoryId)837 private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) { 838 final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon() 839 .appendPath(constraint.toString()) 840 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 841 String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); 842 if (directoryId != null) { 843 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 844 String.valueOf(directoryId)); 845 } 846 if (mAccount != null) { 847 builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name); 848 builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type); 849 } 850 final long start = System.currentTimeMillis(); 851 final Cursor cursor = mContentResolver.query( 852 builder.build(), mQueryMode.getProjection(), null, null, null); 853 final long end = System.currentTimeMillis(); 854 if (DEBUG) { 855 Log.d(TAG, "Time for autocomplete (query: " + constraint 856 + ", directoryId: " + directoryId + ", num_of_results: " 857 + (cursor != null ? cursor.getCount() : "null") + "): " 858 + (end - start) + " ms"); 859 } 860 return cursor; 861 } 862 863 // TODO: This won't be used at all. We should find better way to quit the thread.. 864 /*public void close() { 865 mEntries = null; 866 mPhotoCacheMap.evictAll(); 867 if (!sPhotoHandlerThread.quit()) { 868 Log.w(TAG, "Failed to quit photo handler thread, ignoring it."); 869 } 870 }*/ 871 872 @Override getCount()873 public int getCount() { 874 final List<RecipientEntry> entries = getEntries(); 875 return entries != null ? entries.size() : 0; 876 } 877 878 @Override getItem(int position)879 public RecipientEntry getItem(int position) { 880 return getEntries().get(position); 881 } 882 883 @Override getItemId(int position)884 public long getItemId(int position) { 885 return position; 886 } 887 888 @Override getViewTypeCount()889 public int getViewTypeCount() { 890 return RecipientEntry.ENTRY_TYPE_SIZE; 891 } 892 893 @Override getItemViewType(int position)894 public int getItemViewType(int position) { 895 return getEntries().get(position).getEntryType(); 896 } 897 898 @Override isEnabled(int position)899 public boolean isEnabled(int position) { 900 return getEntries().get(position).isSelectable(); 901 } 902 903 @Override getView(int position, View convertView, ViewGroup parent)904 public View getView(int position, View convertView, ViewGroup parent) { 905 final RecipientEntry entry = getEntries().get(position); 906 907 final String constraint = mCurrentConstraint == null ? null : 908 mCurrentConstraint.toString(); 909 910 return mDropdownChipLayouter.bindView(convertView, parent, entry, position, 911 AdapterType.BASE_RECIPIENT, constraint); 912 } 913 getAccount()914 public Account getAccount() { 915 return mAccount; 916 } 917 918 @Override onPhotoBytesPopulated()919 public void onPhotoBytesPopulated() { 920 // Default implementation does nothing 921 } 922 923 @Override onPhotoBytesAsynchronouslyPopulated()924 public void onPhotoBytesAsynchronouslyPopulated() { 925 notifyDataSetChanged(); 926 } 927 928 @Override onPhotoBytesAsyncLoadFailed()929 public void onPhotoBytesAsyncLoadFailed() { 930 // Default implementation does nothing 931 } 932 } 933