BaseRecipientAdapter.java revision 6b6de6266d3bede33728cf995f1fd5c59ec5a55d
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.graphics.Bitmap; 27import android.graphics.BitmapFactory; 28import android.net.Uri; 29import android.os.Handler; 30import android.os.HandlerThread; 31import android.provider.ContactsContract; 32import android.provider.ContactsContract.CommonDataKinds.Email; 33import android.provider.ContactsContract.CommonDataKinds.Phone; 34import android.provider.ContactsContract.CommonDataKinds.Photo; 35import android.provider.ContactsContract.Contacts; 36import android.provider.ContactsContract.Directory; 37import android.text.TextUtils; 38import android.text.util.Rfc822Token; 39import android.util.Log; 40import android.util.LruCache; 41import android.view.LayoutInflater; 42import android.view.View; 43import android.view.ViewGroup; 44import android.widget.AutoCompleteTextView; 45import android.widget.BaseAdapter; 46import android.widget.Filter; 47import android.widget.Filterable; 48import android.widget.ImageView; 49import android.widget.TextView; 50 51import java.util.ArrayList; 52import java.util.HashSet; 53import java.util.LinkedHashMap; 54import java.util.List; 55import java.util.Map; 56import java.util.Set; 57 58/** 59 * Adapter for showing a recipient list. 60 */ 61public abstract class BaseRecipientAdapter extends BaseAdapter implements Filterable { 62 private static final String TAG = "BaseRecipientAdapter"; 63 private static final boolean DEBUG = false; 64 65 /** 66 * The preferred number of results to be retrieved. This number may be 67 * exceeded if there are several directories configured, because we will use 68 * the same limit for all directories. 69 */ 70 private static final int DEFAULT_PREFERRED_MAX_RESULT_COUNT = 10; 71 72 /** 73 * The number of extra entries requested to allow for duplicates. Duplicates 74 * are removed from the overall result. 75 */ 76 private static final int ALLOWANCE_FOR_DUPLICATES = 5; 77 78 // This is ContactsContract.PRIMARY_ACCOUNT_NAME. Available from ICS as hidden 79 private static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account"; 80 // This is ContactsContract.PRIMARY_ACCOUNT_TYPE. Available from ICS as hidden 81 private static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account"; 82 83 /** The number of photos cached in this Adapter. */ 84 private static final int PHOTO_CACHE_SIZE = 20; 85 86 public static final int QUERY_TYPE_EMAIL = 0; 87 public static final int QUERY_TYPE_PHONE = 1; 88 89 /** 90 * Model object for a {@link Directory} row. 91 */ 92 public final static class DirectorySearchParams { 93 public long directoryId; 94 public String directoryType; 95 public String displayName; 96 public String accountName; 97 public String accountType; 98 public CharSequence constraint; 99 public DirectoryFilter filter; 100 } 101 102 /* package */ static class EmailQuery { 103 public static final String[] PROJECTION = { 104 Contacts.DISPLAY_NAME, // 0 105 Email.DATA, // 1 106 Email.CONTACT_ID, // 2 107 Email._ID, // 3 108 Contacts.PHOTO_THUMBNAIL_URI // 4 109 }; 110 111 public static final int NAME = 0; 112 public static final int ADDRESS = 1; 113 public static final int CONTACT_ID = 2; 114 public static final int DATA_ID = 3; 115 public static final int PHOTO_THUMBNAIL_URI = 4; 116 } 117 118 private static class PhoneQuery { 119 public static final String[] PROJECTION = { 120 Contacts.DISPLAY_NAME, // 0 121 Phone.DATA, // 1 122 Phone.CONTACT_ID, // 2 123 Phone._ID, // 3 124 Contacts.PHOTO_THUMBNAIL_URI // 4 125 }; 126 public static final int NAME = 0; 127 public static final int NUMBER = 1; 128 public static final int CONTACT_ID = 2; 129 public static final int DATA_ID = 3; 130 public static final int PHOTO_THUMBNAIL_URI = 3; 131 } 132 133 private static class PhotoQuery { 134 public static final String[] PROJECTION = { 135 Photo.PHOTO 136 }; 137 138 public static final int PHOTO = 0; 139 } 140 141 private static class DirectoryListQuery { 142 143 public static final Uri URI = 144 Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories"); 145 public static final String[] PROJECTION = { 146 Directory._ID, // 0 147 Directory.ACCOUNT_NAME, // 1 148 Directory.ACCOUNT_TYPE, // 2 149 Directory.DISPLAY_NAME, // 3 150 Directory.PACKAGE_NAME, // 4 151 Directory.TYPE_RESOURCE_ID, // 5 152 }; 153 154 public static final int ID = 0; 155 public static final int ACCOUNT_NAME = 1; 156 public static final int ACCOUNT_TYPE = 2; 157 public static final int DISPLAY_NAME = 3; 158 public static final int PACKAGE_NAME = 4; 159 public static final int TYPE_RESOURCE_ID = 5; 160 } 161 162 /** 163 * An asynchronous filter used for loading two data sets: email rows from the local 164 * contact provider and the list of {@link Directory}'s. 165 */ 166 private final class DefaultFilter extends Filter { 167 168 @Override 169 protected FilterResults performFiltering(CharSequence constraint) { 170 final FilterResults results = new FilterResults(); 171 Cursor cursor = null; 172 if (!TextUtils.isEmpty(constraint)) { 173 cursor = doQuery(constraint, mPreferredMaxResultCount, null); 174 if (cursor != null) { 175 results.count = cursor.getCount(); 176 } 177 } 178 179 // TODO: implement group feature 180 181 final Cursor directoryCursor = mContentResolver.query( 182 DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, null, null, null); 183 184 if (DEBUG && cursor == null) { 185 Log.w(TAG, "null cursor returned for default Email filter query."); 186 } 187 results.values = new Cursor[] { directoryCursor, cursor }; 188 return results; 189 } 190 191 @Override 192 protected void publishResults(final CharSequence constraint, FilterResults results) { 193 if (results.values != null) { 194 final Cursor[] cursors = (Cursor[]) results.values; 195 // Run on one thread. 196 mHandler.post(new Runnable() { 197 @Override 198 public void run() { 199 onFirstDirectoryLoadFinished(constraint, cursors[0], cursors[1]); 200 } 201 }); 202 } 203 results.count = getCount(); 204 } 205 206 @Override 207 public CharSequence convertResultToString(Object resultValue) { 208 final RecipientEntry entry = (RecipientEntry)resultValue; 209 final String displayName = entry.getDisplayName(); 210 final String emailAddress = entry.getDestination(); 211 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { 212 return emailAddress; 213 } else { 214 return new Rfc822Token(displayName, emailAddress, null).toString(); 215 } 216 } 217 } 218 219 /** 220 * An asynchronous filter that performs search in a particular directory. 221 */ 222 private final class DirectoryFilter extends Filter { 223 private final DirectorySearchParams mParams; 224 private int mLimit; 225 226 public DirectoryFilter(DirectorySearchParams params) { 227 this.mParams = params; 228 } 229 230 public synchronized void setLimit(int limit) { 231 this.mLimit = limit; 232 } 233 234 public synchronized int getLimit() { 235 return this.mLimit; 236 } 237 238 @Override 239 protected FilterResults performFiltering(CharSequence constraint) { 240 final FilterResults results = new FilterResults(); 241 if (!TextUtils.isEmpty(constraint)) { 242 final Cursor cursor = doQuery(constraint, getLimit(), mParams.directoryId); 243 if (cursor != null) { 244 results.values = cursor; 245 } 246 } 247 248 // TODO: implement group feature 249 250 return results; 251 } 252 253 @Override 254 protected void publishResults(final CharSequence constraint, FilterResults results) { 255 final Cursor cursor = (Cursor) results.values; 256 mHandler.post(new Runnable() { 257 @Override 258 public void run() { 259 onDirectoryLoadFinished(constraint, mParams, cursor); 260 } 261 }); 262 results.count = getCount(); 263 } 264 } 265 266 private final Context mContext; 267 private final ContentResolver mContentResolver; 268 private final LayoutInflater mInflater; 269 private final int mQueryType; 270 private Account mAccount; 271 private final int mPreferredMaxResultCount; 272 private final Handler mHandler = new Handler(); 273 274 /** 275 * Each destination (an email address or a phone number) with a valid contactId is first 276 * inserted into {@link #mEntryMap} and grouped by the contactId. 277 * Destinations without valid contactId (possible if they aren't in local storage) are stored 278 * in {@link #mNonAggregatedEntries}. 279 * Duplicates are removed using {@link #mExistingDestinations}. 280 * 281 * After having all results from ContentResolver, all elements in mEntryMap are copied to 282 * mEntry, which will be used to find items in this Adapter. If the number of contacts in 283 * mEntries are less than mPreferredMaxResultCount, contacts in 284 * mNonAggregatedEntries are also used. 285 */ 286 private final LinkedHashMap<Long, List<RecipientEntry>> mEntryMap; 287 private final List<RecipientEntry> mNonAggregatedEntries; 288 private final List<RecipientEntry> mEntries; 289 private final Set<String> mExistingDestinations; 290 291 /** 292 * Used to ignore asynchronous queries with a different constraint, which may appear when 293 * users type characters quickly. 294 */ 295 private CharSequence mCurrentConstraint; 296 297 private final HandlerThread mPhotoHandlerThread; 298 private final Handler mPhotoHandler; 299 private final LruCache<Uri, byte[]> mPhotoCacheMap; 300 301 /** 302 * Constructor for email queries. 303 */ 304 public BaseRecipientAdapter(Context context) { 305 this(context, QUERY_TYPE_EMAIL, DEFAULT_PREFERRED_MAX_RESULT_COUNT); 306 } 307 308 public BaseRecipientAdapter(Context context, int queryType) { 309 this(context, queryType, DEFAULT_PREFERRED_MAX_RESULT_COUNT); 310 } 311 312 public BaseRecipientAdapter(Context context, int queryType, int preferredMaxResultCount) { 313 mContext = context; 314 mContentResolver = context.getContentResolver(); 315 mInflater = LayoutInflater.from(context); 316 mQueryType = queryType; 317 mPreferredMaxResultCount = preferredMaxResultCount; 318 mEntryMap = new LinkedHashMap<Long, List<RecipientEntry>>(); 319 mNonAggregatedEntries = new ArrayList<RecipientEntry>(); 320 mEntries = new ArrayList<RecipientEntry>(); 321 mExistingDestinations = new HashSet<String>(); 322 mPhotoHandlerThread = new HandlerThread("photo_handler"); 323 mPhotoHandlerThread.start(); 324 mPhotoHandler = new Handler(mPhotoHandlerThread.getLooper()); 325 mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE); 326 } 327 328 /** 329 * Set the account when known. Causes the search to prioritize contacts from that account. 330 */ 331 public void setAccount(Account account) { 332 mAccount = account; 333 } 334 335 /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */ 336 @Override 337 public Filter getFilter() { 338 return new DefaultFilter(); 339 } 340 341 /** 342 * Handles the result of the initial call, which brings back the list of directories as well 343 * as the search results for the local directories. 344 * 345 * Must be inside a default Looper thread to avoid synchronization problem. 346 */ 347 protected void onFirstDirectoryLoadFinished( 348 CharSequence constraint, Cursor directoryCursor, Cursor defaultDirectoryCursor) { 349 mCurrentConstraint = constraint; 350 351 try { 352 final List<DirectorySearchParams> paramsList; 353 if (directoryCursor != null) { 354 paramsList = setupOtherDirectories(directoryCursor); 355 } else { 356 paramsList = null; 357 } 358 359 int limit = 0; 360 361 if (defaultDirectoryCursor != null) { 362 mEntryMap.clear(); 363 mNonAggregatedEntries.clear(); 364 mExistingDestinations.clear(); 365 putEntriesWithCursor(defaultDirectoryCursor, true); 366 constructEntryList(); 367 limit = mPreferredMaxResultCount - getCount(); 368 } 369 370 if (limit > 0 && paramsList != null) { 371 searchOtherDirectories(constraint, paramsList, limit); 372 } 373 } finally { 374 if (directoryCursor != null) { 375 directoryCursor.close(); 376 } 377 if (defaultDirectoryCursor != null) { 378 defaultDirectoryCursor.close(); 379 } 380 } 381 } 382 383 private List<DirectorySearchParams> setupOtherDirectories(Cursor directoryCursor) { 384 final PackageManager packageManager = mContext.getPackageManager(); 385 final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>(); 386 DirectorySearchParams preferredDirectory = null; 387 while (directoryCursor.moveToNext()) { 388 final long id = directoryCursor.getLong(DirectoryListQuery.ID); 389 390 // Skip the local invisible directory, because the default directory already includes 391 // all local results. 392 if (id == Directory.LOCAL_INVISIBLE) { 393 continue; 394 } 395 396 final DirectorySearchParams params = new DirectorySearchParams(); 397 final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); 398 final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); 399 params.directoryId = id; 400 params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); 401 params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); 402 params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); 403 if (packageName != null && resourceId != 0) { 404 try { 405 final Resources resources = 406 packageManager.getResourcesForApplication(packageName); 407 params.directoryType = resources.getString(resourceId); 408 if (params.directoryType == null) { 409 Log.e(TAG, "Cannot resolve directory name: " 410 + resourceId + "@" + packageName); 411 } 412 } catch (NameNotFoundException e) { 413 Log.e(TAG, "Cannot resolve directory name: " 414 + resourceId + "@" + packageName, e); 415 } 416 } 417 418 // If an account has been provided and we found a directory that 419 // corresponds to that account, place that directory second, directly 420 // underneath the local contacts. 421 if (mAccount != null && mAccount.name.equals(params.accountName) && 422 mAccount.type.equals(params.accountType)) { 423 preferredDirectory = params; 424 } else { 425 paramsList.add(params); 426 } 427 } 428 429 if (preferredDirectory != null) { 430 paramsList.add(1, preferredDirectory); 431 } 432 433 return paramsList; 434 } 435 436 /** 437 * Starts search in other directories 438 */ 439 private void searchOtherDirectories( 440 CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) { 441 final int count = paramsList.size(); 442 // Note: skipping the default partition (index 0), which has already been loaded 443 for (int i = 1; i < count; i++) { 444 final DirectorySearchParams params = paramsList.get(i); 445 params.constraint = constraint; 446 if (params.filter == null) { 447 params.filter = new DirectoryFilter(params); 448 } 449 params.filter.setLimit(limit); 450 params.filter.filter(constraint); 451 } 452 } 453 454 /** Must be inside a default Looper thread to avoid synchronization problem. */ 455 public void onDirectoryLoadFinished( 456 CharSequence constraint, DirectorySearchParams params, Cursor cursor) { 457 if (cursor != null) { 458 try { 459 if (DEBUG) { 460 Log.v(TAG, "finished loading directory \"" + params.displayName + "\"" + 461 " with query " + constraint); 462 } 463 464 // Check if the received result matches the current constraint 465 // If not - the user must have continued typing after the request was issued 466 final boolean usesSameConstraint; 467 usesSameConstraint = TextUtils.equals(constraint, mCurrentConstraint); 468 if (usesSameConstraint) { 469 putEntriesWithCursor(cursor, params.directoryId == Directory.DEFAULT); 470 constructEntryList(); 471 } 472 } finally { 473 cursor.close(); 474 } 475 } 476 } 477 478 /** 479 * Stores each contact information to {@link #mEntryMap}. {@link #mEntries} isn't touched here. 480 * 481 * In order to make the new information available from outside Adapter, 482 * call {@link #constructEntryList()} after this method. 483 */ 484 private void putEntriesWithCursor(Cursor cursor, boolean validContactId) { 485 cursor.move(-1); 486 while (cursor.moveToNext()) { 487 final String displayName; 488 final String destination; 489 final long contactId; 490 final long dataId; 491 final String thumbnailUriString; 492 if (mQueryType == QUERY_TYPE_EMAIL) { 493 displayName = cursor.getString(EmailQuery.NAME); 494 destination = cursor.getString(EmailQuery.ADDRESS); 495 contactId = cursor.getLong(EmailQuery.CONTACT_ID); 496 dataId = cursor.getLong(EmailQuery.DATA_ID); 497 thumbnailUriString = cursor.getString(EmailQuery.PHOTO_THUMBNAIL_URI); 498 } else if (mQueryType == QUERY_TYPE_PHONE) { 499 displayName = cursor.getString(PhoneQuery.NAME); 500 destination = cursor.getString(PhoneQuery.NUMBER); 501 contactId = cursor.getLong(PhoneQuery.CONTACT_ID); 502 dataId = cursor.getLong(PhoneQuery.DATA_ID); 503 thumbnailUriString = cursor.getString(PhoneQuery.PHOTO_THUMBNAIL_URI); 504 } else { 505 throw new IndexOutOfBoundsException("Unexpected query type: " + mQueryType); 506 } 507 508 // Note: At this point each entry doesn't contain have any photo (thus getPhotoBytes() 509 // returns null). 510 511 if (mExistingDestinations.contains(destination)) { 512 continue; 513 } 514 mExistingDestinations.add(destination); 515 516 if (!validContactId) { 517 mNonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry( 518 displayName, destination, contactId, dataId, thumbnailUriString)); 519 } else if (mEntryMap.containsKey(contactId)) { 520 // We already have a section for the person. 521 final List<RecipientEntry> entryList = mEntryMap.get(contactId); 522 entryList.add(RecipientEntry.constructSecondLevelEntry( 523 displayName, destination, contactId, dataId, thumbnailUriString)); 524 } else { 525 final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>(); 526 entryList.add(RecipientEntry.constructTopLevelEntry( 527 displayName, destination, contactId, dataId, thumbnailUriString)); 528 mEntryMap.put(contactId, entryList); 529 } 530 } 531 } 532 533 /** 534 * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to 535 * fetch a cached photo for each contact entry (other than separators), or request another 536 * thread to get one from directories. The thread ({@link #mPhotoHandlerThread}) will 537 * request {@link #notifyDataSetChanged()} after having the photo asynchronously. 538 */ 539 private void constructEntryList() { 540 mEntries.clear(); 541 int validEntryCount = 0; 542 for (Map.Entry<Long, List<RecipientEntry>> mapEntry : mEntryMap.entrySet()) { 543 final List<RecipientEntry> entryList = mapEntry.getValue(); 544 final int size = entryList.size(); 545 for (int i = 0; i < size; i++) { 546 RecipientEntry entry = entryList.get(i); 547 mEntries.add(entry); 548 tryFetchPhoto(entry); 549 validEntryCount++; 550 if (i < size - 1) { 551 mEntries.add(RecipientEntry.SEP_WITHIN_GROUP); 552 } 553 } 554 mEntries.add(RecipientEntry.SEP_NORMAL); 555 if (validEntryCount > mPreferredMaxResultCount) { 556 break; 557 } 558 } 559 if (validEntryCount <= mPreferredMaxResultCount) { 560 for (RecipientEntry entry : mNonAggregatedEntries) { 561 if (validEntryCount > mPreferredMaxResultCount) { 562 break; 563 } 564 mEntries.add(entry); 565 tryFetchPhoto(entry); 566 567 mEntries.add(RecipientEntry.SEP_NORMAL); 568 validEntryCount++; 569 } 570 } 571 572 // Remove last divider 573 if (mEntries.size() > 1) { 574 mEntries.remove(mEntries.size() - 1); 575 } 576 notifyDataSetChanged(); 577 } 578 579 private void tryFetchPhoto(final RecipientEntry entry) { 580 final Uri photoThumbnailUri = entry.getPhotoThumbnailUri(); 581 if (photoThumbnailUri != null) { 582 final byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri); 583 if (photoBytes != null) { 584 entry.setPhotoBytes(photoBytes); 585 // notifyDataSetChanged() should be called by a caller. 586 } else { 587 if (DEBUG) { 588 Log.d(TAG, "No photo cache for " + entry.getDisplayName() 589 + ". Fetch one asynchronously"); 590 } 591 fetchPhotoAsync(entry, photoThumbnailUri); 592 } 593 } 594 } 595 596 private void fetchPhotoAsync(final RecipientEntry entry, final Uri photoThumbnailUri) { 597 mPhotoHandler.post(new Runnable() { 598 @Override 599 public void run() { 600 final Cursor photoCursor = mContentResolver.query( 601 photoThumbnailUri, PhotoQuery.PROJECTION, null, null, null); 602 if (photoCursor != null) { 603 try { 604 if (photoCursor.moveToFirst()) { 605 final byte[] photoBytes = photoCursor.getBlob(PhotoQuery.PHOTO); 606 entry.setPhotoBytes(photoBytes); 607 608 mHandler.post(new Runnable() { 609 @Override 610 public void run() { 611 mPhotoCacheMap.put(photoThumbnailUri, photoBytes); 612 notifyDataSetChanged(); 613 } 614 }); 615 } 616 } finally { 617 photoCursor.close(); 618 } 619 } 620 } 621 }); 622 } 623 624 protected void fetchPhoto(final RecipientEntry entry, final Uri photoThumbnailUri) { 625 byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri); 626 if (photoBytes != null) { 627 entry.setPhotoBytes(photoBytes); 628 return; 629 } 630 final Cursor photoCursor = mContentResolver.query(photoThumbnailUri, PhotoQuery.PROJECTION, 631 null, null, null); 632 if (photoCursor != null) { 633 try { 634 if (photoCursor.moveToFirst()) { 635 photoBytes = photoCursor.getBlob(PhotoQuery.PHOTO); 636 entry.setPhotoBytes(photoBytes); 637 mPhotoCacheMap.put(photoThumbnailUri, photoBytes); 638 } 639 } finally { 640 photoCursor.close(); 641 } 642 } 643 } 644 645 private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) { 646 final Cursor cursor; 647 if (mQueryType == QUERY_TYPE_EMAIL) { 648 final Uri.Builder builder = Email.CONTENT_FILTER_URI.buildUpon() 649 .appendPath(constraint.toString()) 650 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 651 String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); 652 if (directoryId != null) { 653 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 654 String.valueOf(directoryId)); 655 } 656 if (mAccount != null) { 657 builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name); 658 builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type); 659 } 660 cursor = mContentResolver.query( 661 builder.build(), EmailQuery.PROJECTION, null, null, null); 662 } else if (mQueryType == QUERY_TYPE_PHONE){ 663 final Uri.Builder builder = Phone.CONTENT_FILTER_URI.buildUpon() 664 .appendPath(constraint.toString()) 665 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 666 String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); 667 if (directoryId != null) { 668 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 669 String.valueOf(directoryId)); 670 } 671 if (mAccount != null) { 672 builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name); 673 builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type); 674 } 675 cursor = mContentResolver.query( 676 builder.build(), PhoneQuery.PROJECTION, null, null, null); 677 } else { 678 cursor = null; 679 } 680 return cursor; 681 } 682 683 public void close() { 684 mEntryMap.clear(); 685 mNonAggregatedEntries.clear(); 686 mExistingDestinations.clear(); 687 mEntries.clear(); 688 mPhotoCacheMap.evictAll(); 689 if (!mPhotoHandlerThread.quit()) { 690 Log.w(TAG, "Failed to quit photo handler thread, ignoring it."); 691 } 692 } 693 694 @Override 695 public int getCount() { 696 return mEntries.size(); 697 } 698 699 @Override 700 public Object getItem(int position) { 701 return mEntries.get(position); 702 } 703 704 @Override 705 public long getItemId(int position) { 706 return position; 707 } 708 709 @Override 710 public int getViewTypeCount() { 711 return RecipientEntry.ENTRY_TYPE_SIZE; 712 } 713 714 @Override 715 public int getItemViewType(int position) { 716 return mEntries.get(position).getEntryType(); 717 } 718 719 @Override 720 public View getView(int position, View convertView, ViewGroup parent) { 721 final RecipientEntry entry = mEntries.get(position); 722 switch (entry.getEntryType()) { 723 case RecipientEntry.ENTRY_TYPE_SEP_NORMAL: { 724 return convertView != null ? convertView 725 : mInflater.inflate(getSeparatorLayout(), parent, false); 726 } 727 case RecipientEntry.ENTRY_TYPE_SEP_WITHIN_GROUP: { 728 return convertView != null ? convertView 729 : mInflater.inflate(getSeparatorWithinGroupLayout(), parent, false); 730 } 731 default: { 732 String displayName = entry.getDisplayName(); 733 String emailAddress = entry.getDestination(); 734 if (TextUtils.isEmpty(displayName) 735 || TextUtils.equals(displayName, emailAddress)) { 736 displayName = emailAddress; 737 emailAddress = null; 738 } 739 740 final View itemView = convertView != null ? convertView 741 : mInflater.inflate(getItemLayout(), parent, false); 742 final TextView displayNameView = 743 (TextView)itemView.findViewById(getDisplayNameId()); 744 final TextView emailAddressView = 745 (TextView)itemView.findViewById(getDestinationId()); 746 final ImageView imageView = (ImageView)itemView.findViewById(getPhotoId()); 747 displayNameView.setText(displayName); 748 if (!TextUtils.isEmpty(emailAddress)) { 749 emailAddressView.setText(emailAddress); 750 } else { 751 emailAddressView.setText(null); 752 } 753 if (entry.isFirstLevel()) { 754 displayNameView.setVisibility(View.VISIBLE); 755 if (imageView != null) { 756 imageView.setVisibility(View.VISIBLE); 757 final byte[] photoBytes = entry.getPhotoBytes(); 758 if (photoBytes != null && imageView != null) { 759 final Bitmap photo = BitmapFactory.decodeByteArray( 760 photoBytes, 0, photoBytes.length); 761 imageView.setImageBitmap(photo); 762 } else { 763 imageView.setImageResource(getDefaultPhotoResource()); 764 } 765 } 766 } else { 767 displayNameView.setVisibility(View.GONE); 768 if (imageView != null) imageView.setVisibility(View.GONE); 769 } 770 return itemView; 771 } 772 } 773 } 774 775 /** 776 * Returns a layout id for each item inside auto-complete list. 777 * 778 * Each View must contain two TextViews (for display name and destination) and one ImageView 779 * (for photo). Ids for those should be available via {@link #getDisplayNameId()}, 780 * {@link #getDestinationId()}, and {@link #getPhotoId()}. 781 */ 782 protected abstract int getItemLayout(); 783 /** Returns a layout id for a separator dividing two person or groups. */ 784 protected abstract int getSeparatorLayout(); 785 /** 786 * Returns a layout id for a separator dividing two destinations for a same person or group. 787 */ 788 protected abstract int getSeparatorWithinGroupLayout(); 789 790 /** 791 * Returns a resource ID representing an image which should be shown when ther's no relevant 792 * photo is available. 793 */ 794 protected abstract int getDefaultPhotoResource(); 795 796 /** 797 * Returns an id for TextView in an item View for showing a display name. In default 798 * {@link android.R.id#text1} is returned. 799 */ 800 protected int getDisplayNameId() { 801 return android.R.id.text1; 802 } 803 804 /** 805 * Returns an id for TextView in an item View for showing a destination 806 * (an email address or a phone number). 807 * In default {@link android.R.id#text2} is returned. 808 */ 809 protected int getDestinationId() { 810 return android.R.id.text2; 811 } 812 813 /** 814 * Returns an id for ImageView in an item View for showing photo image for a person. In default 815 * {@link android.R.id#icon} is returned. 816 */ 817 protected int getPhotoId() { 818 return android.R.id.icon; 819 } 820} 821