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