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 = entries.size(); 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 cacheCurrentEntriesIfNeeded(defaultFilterResult.entries.size(), 285 defaultFilterResult.paramsList == null ? 0 : 286 defaultFilterResult.paramsList.size()); 287 288 updateEntries(defaultFilterResult.entries); 289 290 // We need to search other remote directories, doing other Filter requests. 291 if (defaultFilterResult.paramsList != null) { 292 final int limit = mPreferredMaxResultCount - 293 defaultFilterResult.existingDestinations.size(); 294 startSearchOtherDirectories(constraint, defaultFilterResult.paramsList, limit); 295 } 296 } else { 297 updateEntries(Collections.<RecipientEntry>emptyList()); 298 } 299 } 300 301 @Override convertResultToString(Object resultValue)302 public CharSequence convertResultToString(Object resultValue) { 303 final RecipientEntry entry = (RecipientEntry)resultValue; 304 final String displayName = entry.getDisplayName(); 305 final String emailAddress = entry.getDestination(); 306 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { 307 return emailAddress; 308 } else { 309 return new Rfc822Token(displayName, emailAddress, null).toString(); 310 } 311 } 312 } 313 searchOtherDirectories(Set<String> existingDestinations)314 protected List<DirectorySearchParams> searchOtherDirectories(Set<String> existingDestinations) { 315 // After having local results, check the size of results. If the results are 316 // not enough, we search remote directories, which will take longer time. 317 final int limit = mPreferredMaxResultCount - existingDestinations.size(); 318 if (limit > 0) { 319 if (DEBUG) { 320 Log.d(TAG, "More entries should be needed (current: " 321 + existingDestinations.size() 322 + ", remaining limit: " + limit + ") "); 323 } 324 Cursor directoryCursor = null; 325 try { 326 directoryCursor = mContentResolver.query( 327 DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, 328 null, null, null); 329 return setupOtherDirectories(mContext, directoryCursor, mAccount); 330 } finally { 331 if (directoryCursor != null) { 332 directoryCursor.close(); 333 } 334 } 335 } else { 336 // We don't need to search other directories. 337 return null; 338 } 339 } 340 341 /** 342 * An asynchronous filter that performs search in a particular directory. 343 */ 344 protected class DirectoryFilter extends Filter { 345 private final DirectorySearchParams mParams; 346 private int mLimit; 347 DirectoryFilter(DirectorySearchParams params)348 public DirectoryFilter(DirectorySearchParams params) { 349 mParams = params; 350 } 351 setLimit(int limit)352 public synchronized void setLimit(int limit) { 353 this.mLimit = limit; 354 } 355 getLimit()356 public synchronized int getLimit() { 357 return this.mLimit; 358 } 359 360 @Override performFiltering(CharSequence constraint)361 protected FilterResults performFiltering(CharSequence constraint) { 362 if (DEBUG) { 363 Log.d(TAG, "DirectoryFilter#performFiltering. directoryId: " + mParams.directoryId 364 + ", constraint: " + constraint + ", thread: " + Thread.currentThread()); 365 } 366 final FilterResults results = new FilterResults(); 367 results.values = null; 368 results.count = 0; 369 370 if (!TextUtils.isEmpty(constraint)) { 371 final ArrayList<TemporaryEntry> tempEntries = new ArrayList<TemporaryEntry>(); 372 373 Cursor cursor = null; 374 try { 375 // We don't want to pass this Cursor object to UI thread (b/5017608). 376 // Assuming the result should contain fairly small results (at most ~10), 377 // We just copy everything to local structure. 378 cursor = doQuery(constraint, getLimit(), mParams.directoryId); 379 380 if (cursor != null) { 381 while (cursor.moveToNext()) { 382 tempEntries.add(new TemporaryEntry(cursor, mParams.directoryId)); 383 } 384 } 385 } finally { 386 if (cursor != null) { 387 cursor.close(); 388 } 389 } 390 if (!tempEntries.isEmpty()) { 391 results.values = tempEntries; 392 results.count = tempEntries.size(); 393 } 394 } 395 396 if (DEBUG) { 397 Log.v(TAG, "finished loading directory \"" + mParams.displayName + "\"" + 398 " with query " + constraint); 399 } 400 401 return results; 402 } 403 404 @Override publishResults(final CharSequence constraint, FilterResults results)405 protected void publishResults(final CharSequence constraint, FilterResults results) { 406 if (DEBUG) { 407 Log.d(TAG, "DirectoryFilter#publishResult. constraint: " + constraint 408 + ", mCurrentConstraint: " + mCurrentConstraint); 409 } 410 mDelayedMessageHandler.removeDelayedLoadMessage(); 411 // Check if the received result matches the current constraint 412 // If not - the user must have continued typing after the request was issued, which 413 // means several member variables (like mRemainingDirectoryLoad) are already 414 // overwritten so shouldn't be touched here anymore. 415 if (TextUtils.equals(constraint, mCurrentConstraint)) { 416 if (results.count > 0) { 417 @SuppressWarnings("unchecked") 418 final ArrayList<TemporaryEntry> tempEntries = 419 (ArrayList<TemporaryEntry>) results.values; 420 421 for (TemporaryEntry tempEntry : tempEntries) { 422 putOneEntry(tempEntry, mParams.directoryId == Directory.DEFAULT); 423 } 424 } 425 426 // If there are remaining directories, set up delayed message again. 427 mRemainingDirectoryCount--; 428 if (mRemainingDirectoryCount > 0) { 429 if (DEBUG) { 430 Log.d(TAG, "Resend delayed load message. Current mRemainingDirectoryLoad: " 431 + mRemainingDirectoryCount); 432 } 433 mDelayedMessageHandler.sendDelayedLoadMessage(); 434 } 435 436 // If this directory result has some items, or there are no more directories that 437 // we are waiting for, clear the temp results 438 if (results.count > 0 || mRemainingDirectoryCount == 0) { 439 // Clear the temp entries 440 clearTempEntries(); 441 } 442 } 443 444 // Show the list again without "waiting" message. 445 updateEntries(constructEntryList()); 446 } 447 } 448 449 private final Context mContext; 450 private final ContentResolver mContentResolver; 451 private Account mAccount; 452 protected final int mPreferredMaxResultCount; 453 private DropdownChipLayouter mDropdownChipLayouter; 454 455 /** 456 * {@link #mEntries} is responsible for showing every result for this Adapter. To 457 * construct it, we use {@link #mEntryMap}, {@link #mNonAggregatedEntries}, and 458 * {@link #mExistingDestinations}. 459 * 460 * First, each destination (an email address or a phone number) with a valid contactId is 461 * inserted into {@link #mEntryMap} and grouped by the contactId. Destinations without valid 462 * contactId (possible if they aren't in local storage) are stored in 463 * {@link #mNonAggregatedEntries}. 464 * Duplicates are removed using {@link #mExistingDestinations}. 465 * 466 * After having all results from Cursor objects, all destinations in mEntryMap are copied to 467 * {@link #mEntries}. If the number of destinations is not enough (i.e. less than 468 * {@link #mPreferredMaxResultCount}), destinations in mNonAggregatedEntries are also used. 469 * 470 * These variables are only used in UI thread, thus should not be touched in 471 * performFiltering() methods. 472 */ 473 private LinkedHashMap<Long, List<RecipientEntry>> mEntryMap; 474 private List<RecipientEntry> mNonAggregatedEntries; 475 private Set<String> mExistingDestinations; 476 /** Note: use {@link #updateEntries(List)} to update this variable. */ 477 private List<RecipientEntry> mEntries; 478 private List<RecipientEntry> mTempEntries; 479 480 /** The number of directories this adapter is waiting for results. */ 481 private int mRemainingDirectoryCount; 482 483 /** 484 * Used to ignore asynchronous queries with a different constraint, which may happen when 485 * users type characters quickly. 486 */ 487 protected CharSequence mCurrentConstraint; 488 489 /** 490 * Performs all photo querying as well as caching for repeated lookups. 491 */ 492 private PhotoManager mPhotoManager; 493 494 /** 495 * Handler specific for maintaining "Waiting for more contacts" message, which will be shown 496 * when: 497 * - there are directories to be searched 498 * - results from directories are slow to come 499 */ 500 private final class DelayedMessageHandler extends Handler { 501 @Override handleMessage(Message msg)502 public void handleMessage(Message msg) { 503 if (mRemainingDirectoryCount > 0) { 504 updateEntries(constructEntryList()); 505 } 506 } 507 sendDelayedLoadMessage()508 public void sendDelayedLoadMessage() { 509 sendMessageDelayed(obtainMessage(MESSAGE_SEARCH_PENDING, 0, 0, null), 510 MESSAGE_SEARCH_PENDING_DELAY); 511 } 512 removeDelayedLoadMessage()513 public void removeDelayedLoadMessage() { 514 removeMessages(MESSAGE_SEARCH_PENDING); 515 } 516 } 517 518 private final DelayedMessageHandler mDelayedMessageHandler = new DelayedMessageHandler(); 519 520 private EntriesUpdatedObserver mEntriesUpdatedObserver; 521 522 /** 523 * Constructor for email queries. 524 */ BaseRecipientAdapter(Context context)525 public BaseRecipientAdapter(Context context) { 526 this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, QUERY_TYPE_EMAIL); 527 } 528 BaseRecipientAdapter(Context context, int preferredMaxResultCount)529 public BaseRecipientAdapter(Context context, int preferredMaxResultCount) { 530 this(context, preferredMaxResultCount, QUERY_TYPE_EMAIL); 531 } 532 BaseRecipientAdapter(int queryMode, Context context)533 public BaseRecipientAdapter(int queryMode, Context context) { 534 this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, queryMode); 535 } 536 BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount)537 public BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount) { 538 this(context, preferredMaxResultCount, queryMode); 539 } 540 BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode)541 public BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode) { 542 mContext = context; 543 mContentResolver = context.getContentResolver(); 544 mPreferredMaxResultCount = preferredMaxResultCount; 545 mPhotoManager = new DefaultPhotoManager(mContentResolver); 546 mQueryType = queryMode; 547 548 if (queryMode == QUERY_TYPE_EMAIL) { 549 mQueryMode = Queries.EMAIL; 550 } else if (queryMode == QUERY_TYPE_PHONE) { 551 mQueryMode = Queries.PHONE; 552 } else { 553 mQueryMode = Queries.EMAIL; 554 Log.e(TAG, "Unsupported query type: " + queryMode); 555 } 556 } 557 getContext()558 public Context getContext() { 559 return mContext; 560 } 561 getQueryType()562 public int getQueryType() { 563 return mQueryType; 564 } 565 setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter)566 public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) { 567 mDropdownChipLayouter = dropdownChipLayouter; 568 mDropdownChipLayouter.setQuery(mQueryMode); 569 } 570 getDropdownChipLayouter()571 public DropdownChipLayouter getDropdownChipLayouter() { 572 return mDropdownChipLayouter; 573 } 574 575 /** 576 * Enables overriding the default photo manager that is used. 577 */ setPhotoManager(PhotoManager photoManager)578 public void setPhotoManager(PhotoManager photoManager) { 579 mPhotoManager = photoManager; 580 } 581 getPhotoManager()582 public PhotoManager getPhotoManager() { 583 return mPhotoManager; 584 } 585 586 /** 587 * If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter} 588 * instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when 589 * clicking on a chip. Default implementation returns {@code false}. 590 */ forceShowAddress()591 public boolean forceShowAddress() { 592 return false; 593 } 594 595 /** 596 * Used to replace email addresses with chips. Default behavior 597 * queries the ContactsProvider for contact information about the contact. 598 * Derived classes should override this method if they wish to use a 599 * new data source. 600 * @param inAddresses addresses to query 601 * @param callback callback to return results in case of success or failure 602 */ getMatchingRecipients(ArrayList<String> inAddresses, RecipientAlternatesAdapter.RecipientMatchCallback callback)603 public void getMatchingRecipients(ArrayList<String> inAddresses, 604 RecipientAlternatesAdapter.RecipientMatchCallback callback) { 605 RecipientAlternatesAdapter.getMatchingRecipients( 606 getContext(), this, inAddresses, getAccount(), callback); 607 } 608 609 /** 610 * Set the account when known. Causes the search to prioritize contacts from that account. 611 */ 612 @Override setAccount(Account account)613 public void setAccount(Account account) { 614 mAccount = account; 615 } 616 617 /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */ 618 @Override getFilter()619 public Filter getFilter() { 620 return new DefaultFilter(); 621 } 622 623 /** 624 * An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows 625 * additional sources of contacts to be considered as matching recipients. 626 * @param addresses A set of addresses to be matched 627 * @return A list of matches or null if none found 628 */ getMatchingRecipients(Set<String> addresses)629 public Map<String, RecipientEntry> getMatchingRecipients(Set<String> addresses) { 630 return null; 631 } 632 setupOtherDirectories(Context context, Cursor directoryCursor, Account account)633 public static List<DirectorySearchParams> setupOtherDirectories(Context context, 634 Cursor directoryCursor, Account account) { 635 final PackageManager packageManager = context.getPackageManager(); 636 final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>(); 637 DirectorySearchParams preferredDirectory = null; 638 while (directoryCursor.moveToNext()) { 639 final long id = directoryCursor.getLong(DirectoryListQuery.ID); 640 641 // Skip the local invisible directory, because the default directory already includes 642 // all local results. 643 if (id == Directory.LOCAL_INVISIBLE) { 644 continue; 645 } 646 647 final DirectorySearchParams params = new DirectorySearchParams(); 648 final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); 649 final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); 650 params.directoryId = id; 651 params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); 652 params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); 653 params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); 654 if (packageName != null && resourceId != 0) { 655 try { 656 final Resources resources = 657 packageManager.getResourcesForApplication(packageName); 658 params.directoryType = resources.getString(resourceId); 659 if (params.directoryType == null) { 660 Log.e(TAG, "Cannot resolve directory name: " 661 + resourceId + "@" + packageName); 662 } 663 } catch (NameNotFoundException e) { 664 Log.e(TAG, "Cannot resolve directory name: " 665 + resourceId + "@" + packageName, e); 666 } 667 } 668 669 // If an account has been provided and we found a directory that 670 // corresponds to that account, place that directory second, directly 671 // underneath the local contacts. 672 if (account != null && account.name.equals(params.accountName) && 673 account.type.equals(params.accountType)) { 674 preferredDirectory = params; 675 } else { 676 paramsList.add(params); 677 } 678 } 679 680 if (preferredDirectory != null) { 681 paramsList.add(1, preferredDirectory); 682 } 683 684 return paramsList; 685 } 686 687 /** 688 * Starts search in other directories using {@link Filter}. Results will be handled in 689 * {@link DirectoryFilter}. 690 */ startSearchOtherDirectories( CharSequence constraint, List<DirectorySearchParams> paramsList, int limit)691 protected void startSearchOtherDirectories( 692 CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) { 693 final int count = paramsList.size(); 694 // Note: skipping the default partition (index 0), which has already been loaded 695 for (int i = 1; i < count; i++) { 696 final DirectorySearchParams params = paramsList.get(i); 697 params.constraint = constraint; 698 if (params.filter == null) { 699 params.filter = new DirectoryFilter(params); 700 } 701 params.filter.setLimit(limit); 702 params.filter.filter(constraint); 703 } 704 705 // Directory search started. We may show "waiting" message if directory results are slow 706 // enough. 707 mRemainingDirectoryCount = count - 1; 708 mDelayedMessageHandler.sendDelayedLoadMessage(); 709 } 710 711 /** 712 * Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter} 713 * wants to add an additional entry to the results. Derived classes should override 714 * this method if they are not using the default data structures provided by 715 * {@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their 716 * own data structures to store and collate data. 717 * @param entry the entry being added 718 * @param isAggregatedEntry 719 */ putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry)720 protected void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry) { 721 putOneEntry(entry, isAggregatedEntry, 722 mEntryMap, mNonAggregatedEntries, mExistingDestinations); 723 } 724 putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry, LinkedHashMap<Long, List<RecipientEntry>> entryMap, List<RecipientEntry> nonAggregatedEntries, Set<String> existingDestinations)725 private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry, 726 LinkedHashMap<Long, List<RecipientEntry>> entryMap, 727 List<RecipientEntry> nonAggregatedEntries, 728 Set<String> existingDestinations) { 729 if (existingDestinations.contains(entry.destination)) { 730 return; 731 } 732 733 existingDestinations.add(entry.destination); 734 735 if (!isAggregatedEntry) { 736 nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry( 737 entry.displayName, 738 entry.displayNameSource, 739 entry.destination, entry.destinationType, entry.destinationLabel, 740 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 741 true, entry.lookupKey)); 742 } else if (entryMap.containsKey(entry.contactId)) { 743 // We already have a section for the person. 744 final List<RecipientEntry> entryList = entryMap.get(entry.contactId); 745 entryList.add(RecipientEntry.constructSecondLevelEntry( 746 entry.displayName, 747 entry.displayNameSource, 748 entry.destination, entry.destinationType, entry.destinationLabel, 749 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 750 true, entry.lookupKey)); 751 } else { 752 final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>(); 753 entryList.add(RecipientEntry.constructTopLevelEntry( 754 entry.displayName, 755 entry.displayNameSource, 756 entry.destination, entry.destinationType, entry.destinationLabel, 757 entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, 758 true, entry.lookupKey)); 759 entryMap.put(entry.contactId, entryList); 760 } 761 } 762 763 /** 764 * Returns the actual list to use for this Adapter. Derived classes 765 * should override this method if overriding how the adapter stores and collates 766 * data. 767 */ constructEntryList()768 protected List<RecipientEntry> constructEntryList() { 769 return constructEntryList(mEntryMap, mNonAggregatedEntries); 770 } 771 772 /** 773 * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to 774 * fetch a cached photo for each contact entry (other than separators), or request another 775 * thread to get one from directories. 776 */ constructEntryList( LinkedHashMap<Long, List<RecipientEntry>> entryMap, List<RecipientEntry> nonAggregatedEntries)777 private List<RecipientEntry> constructEntryList( 778 LinkedHashMap<Long, List<RecipientEntry>> entryMap, 779 List<RecipientEntry> nonAggregatedEntries) { 780 final List<RecipientEntry> entries = new ArrayList<RecipientEntry>(); 781 int validEntryCount = 0; 782 for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) { 783 final List<RecipientEntry> entryList = mapEntry.getValue(); 784 final int size = entryList.size(); 785 for (int i = 0; i < size; i++) { 786 RecipientEntry entry = entryList.get(i); 787 entries.add(entry); 788 mPhotoManager.populatePhotoBytesAsync(entry, this); 789 validEntryCount++; 790 } 791 if (validEntryCount > mPreferredMaxResultCount) { 792 break; 793 } 794 } 795 if (validEntryCount <= mPreferredMaxResultCount) { 796 for (RecipientEntry entry : nonAggregatedEntries) { 797 if (validEntryCount > mPreferredMaxResultCount) { 798 break; 799 } 800 entries.add(entry); 801 mPhotoManager.populatePhotoBytesAsync(entry, this); 802 validEntryCount++; 803 } 804 } 805 806 return entries; 807 } 808 809 810 public interface EntriesUpdatedObserver { onChanged(List<RecipientEntry> entries)811 public void onChanged(List<RecipientEntry> entries); 812 } 813 registerUpdateObserver(EntriesUpdatedObserver observer)814 public void registerUpdateObserver(EntriesUpdatedObserver observer) { 815 mEntriesUpdatedObserver = observer; 816 } 817 818 /** Resets {@link #mEntries} and notify the event to its parent ListView. */ updateEntries(List<RecipientEntry> newEntries)819 protected void updateEntries(List<RecipientEntry> newEntries) { 820 mEntries = newEntries; 821 mEntriesUpdatedObserver.onChanged(newEntries); 822 notifyDataSetChanged(); 823 } 824 825 /** 826 * If there are no local results and we are searching alternate results, 827 * in the new result set, cache off what had been shown to the user for use until 828 * the first directory result is returned 829 * @param newEntryCount number of newly loaded entries 830 * @param paramListCount number of alternate filters it will search (including the current one). 831 */ cacheCurrentEntriesIfNeeded(int newEntryCount, int paramListCount)832 protected void cacheCurrentEntriesIfNeeded(int newEntryCount, int paramListCount) { 833 if (newEntryCount == 0 && paramListCount > 1) { 834 cacheCurrentEntries(); 835 } 836 } 837 cacheCurrentEntries()838 protected void cacheCurrentEntries() { 839 mTempEntries = mEntries; 840 } 841 clearTempEntries()842 protected void clearTempEntries() { 843 mTempEntries = null; 844 } 845 getEntries()846 protected List<RecipientEntry> getEntries() { 847 return mTempEntries != null ? mTempEntries : mEntries; 848 } 849 fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb)850 protected void fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb) { 851 mPhotoManager.populatePhotoBytesAsync(entry, cb); 852 } 853 doQuery(CharSequence constraint, int limit, Long directoryId)854 private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) { 855 final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon() 856 .appendPath(constraint.toString()) 857 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 858 String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); 859 if (directoryId != null) { 860 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 861 String.valueOf(directoryId)); 862 } 863 if (mAccount != null) { 864 builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name); 865 builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type); 866 } 867 final long start = System.currentTimeMillis(); 868 final Cursor cursor = mContentResolver.query( 869 builder.build(), mQueryMode.getProjection(), null, null, null); 870 final long end = System.currentTimeMillis(); 871 if (DEBUG) { 872 Log.d(TAG, "Time for autocomplete (query: " + constraint 873 + ", directoryId: " + directoryId + ", num_of_results: " 874 + (cursor != null ? cursor.getCount() : "null") + "): " 875 + (end - start) + " ms"); 876 } 877 return cursor; 878 } 879 880 // TODO: This won't be used at all. We should find better way to quit the thread.. 881 /*public void close() { 882 mEntries = null; 883 mPhotoCacheMap.evictAll(); 884 if (!sPhotoHandlerThread.quit()) { 885 Log.w(TAG, "Failed to quit photo handler thread, ignoring it."); 886 } 887 }*/ 888 889 @Override getCount()890 public int getCount() { 891 final List<RecipientEntry> entries = getEntries(); 892 return entries != null ? entries.size() : 0; 893 } 894 895 @Override getItem(int position)896 public RecipientEntry getItem(int position) { 897 return getEntries().get(position); 898 } 899 900 @Override getItemId(int position)901 public long getItemId(int position) { 902 return position; 903 } 904 905 @Override getViewTypeCount()906 public int getViewTypeCount() { 907 return RecipientEntry.ENTRY_TYPE_SIZE; 908 } 909 910 @Override getItemViewType(int position)911 public int getItemViewType(int position) { 912 return getEntries().get(position).getEntryType(); 913 } 914 915 @Override isEnabled(int position)916 public boolean isEnabled(int position) { 917 return getEntries().get(position).isSelectable(); 918 } 919 920 @Override getView(int position, View convertView, ViewGroup parent)921 public View getView(int position, View convertView, ViewGroup parent) { 922 final RecipientEntry entry = getEntries().get(position); 923 924 final String constraint = mCurrentConstraint == null ? null : 925 mCurrentConstraint.toString(); 926 927 return mDropdownChipLayouter.bindView(convertView, parent, entry, position, 928 AdapterType.BASE_RECIPIENT, constraint); 929 } 930 getAccount()931 public Account getAccount() { 932 return mAccount; 933 } 934 935 @Override onPhotoBytesPopulated()936 public void onPhotoBytesPopulated() { 937 // Default implementation does nothing 938 } 939 940 @Override onPhotoBytesAsynchronouslyPopulated()941 public void onPhotoBytesAsynchronouslyPopulated() { 942 notifyDataSetChanged(); 943 } 944 945 @Override onPhotoBytesAsyncLoadFailed()946 public void onPhotoBytesAsyncLoadFailed() { 947 // Default implementation does nothing 948 } 949 } 950