diff options
author | Mindy Pereira <mindyp@google.com> | 2011-05-27 15:23:46 -0700 |
---|---|---|
committer | Mindy Pereira <mindyp@google.com> | 2011-05-27 15:37:12 -0700 |
commit | f026dfb761c894942354060746a8ab7dd563386c (patch) | |
tree | 6bd938291be48c144840f4f89c044be43fcd15e1 /chips/src/com/android/ex/chips/BaseRecipientAdapter.java | |
parent | 4334eac50ad87009874cb8b9af0572b0e2d8d4c8 (diff) | |
download | android_frameworks_ex-f026dfb761c894942354060746a8ab7dd563386c.tar.gz android_frameworks_ex-f026dfb761c894942354060746a8ab7dd563386c.tar.bz2 android_frameworks_ex-f026dfb761c894942354060746a8ab7dd563386c.zip |
Make chips included source instead of static java library.
Get rid of abstract methods in BaseRecipientAdapter
Update makefile
Change-Id: I3415c73446c605e69c0c2145e98a7e66abc33a6e
Diffstat (limited to 'chips/src/com/android/ex/chips/BaseRecipientAdapter.java')
-rw-r--r-- | chips/src/com/android/ex/chips/BaseRecipientAdapter.java | 789 |
1 files changed, 789 insertions, 0 deletions
diff --git a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java new file mode 100644 index 0000000..27bc8f9 --- /dev/null +++ b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java @@ -0,0 +1,789 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ex.chips; + +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.text.util.Rfc822Token; +import android.util.Log; +import android.util.LruCache; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AutoCompleteTextView; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jp.mowanet.chipexp.R; + +/** + * Adapter for showing a recipient list. + */ +public class BaseRecipientAdapter extends BaseAdapter implements Filterable { + private static final String TAG = "BaseRecipientAdapter"; + private static final boolean DEBUG = false; + + /** + * The preferred number of results to be retrieved. This number may be + * exceeded if there are several directories configured, because we will use + * the same limit for all directories. + */ + private static final int DEFAULT_PREFERRED_MAX_RESULT_COUNT = 10; + + /** + * The number of extra entries requested to allow for duplicates. Duplicates + * are removed from the overall result. + */ + private static final int ALLOWANCE_FOR_DUPLICATES = 5; + + /** The number of photos cached in this Adapter. */ + private static final int PHOTO_CACHE_SIZE = 20; + + public static final int QUERY_TYPE_EMAIL = 0; + public static final int QUERY_TYPE_PHONE = 1; + + /** + * Model object for a {@link Directory} row. + */ + public final static class DirectorySearchParams { + public long directoryId; + public String directoryType; + public String displayName; + public String accountName; + public String accountType; + public CharSequence constraint; + public DirectoryFilter filter; + } + + private static class EmailQuery { + public static final String[] PROJECTION = { + Contacts.DISPLAY_NAME, // 0 + Email.DATA, // 1 + Email.CONTACT_ID, // 2 + Contacts.PHOTO_THUMBNAIL_URI // 3 + }; + + public static final int NAME = 0; + public static final int ADDRESS = 1; + public static final int CONTACT_ID = 2; + public static final int PHOTO_THUMBNAIL_URI = 3; + } + + private static class PhoneQuery { + public static final String[] PROJECTION = { + Contacts.DISPLAY_NAME, // 0 + Phone.DATA, // 1 + Phone.CONTACT_ID, // 2 + Contacts.PHOTO_THUMBNAIL_URI // 3 + }; + public static final int NAME = 0; + public static final int NUMBER = 1; + public static final int CONTACT_ID = 2; + public static final int PHOTO_THUMBNAIL_URI = 3; + } + + private static class PhotoQuery { + public static final String[] PROJECTION = { + Photo.PHOTO + }; + + public static final int PHOTO = 0; + } + + private static class DirectoryListQuery { + + public static final Uri URI = + Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories"); + public static final String[] PROJECTION = { + Directory._ID, // 0 + Directory.ACCOUNT_NAME, // 1 + Directory.ACCOUNT_TYPE, // 2 + Directory.DISPLAY_NAME, // 3 + Directory.PACKAGE_NAME, // 4 + Directory.TYPE_RESOURCE_ID, // 5 + }; + + public static final int ID = 0; + public static final int ACCOUNT_NAME = 1; + public static final int ACCOUNT_TYPE = 2; + public static final int DISPLAY_NAME = 3; + public static final int PACKAGE_NAME = 4; + public static final int TYPE_RESOURCE_ID = 5; + } + + /** + * An asynchronous filter used for loading two data sets: email rows from the local + * contact provider and the list of {@link Directory}'s. + */ + private final class DefaultFilter extends Filter { + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + final FilterResults results = new FilterResults(); + Cursor cursor = null; + if (!TextUtils.isEmpty(constraint)) { + cursor = doQuery(constraint, mPreferredMaxResultCount, null); + if (cursor != null) { + results.count = cursor.getCount(); + } + } + + // TODO: implement group feature + + final Cursor directoryCursor = mContentResolver.query( + DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, null, null, null); + + if (DEBUG && cursor == null) { + Log.w(TAG, "null cursor returned for default Email filter query."); + } + results.values = new Cursor[] { directoryCursor, cursor }; + return results; + } + + @Override + protected void publishResults(final CharSequence constraint, FilterResults results) { + if (results.values != null) { + final Cursor[] cursors = (Cursor[]) results.values; + // Run on one thread. + mHandler.post(new Runnable() { + @Override + public void run() { + onFirstDirectoryLoadFinished(constraint, cursors[0], cursors[1]); + } + }); + } + results.count = getCount(); + } + + @Override + public CharSequence convertResultToString(Object resultValue) { + final RecipientListEntry entry = (RecipientListEntry)resultValue; + final String displayName = entry.getDisplayName(); + final String emailAddress = entry.getDestination(); + if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { + return emailAddress; + } else { + return new Rfc822Token(displayName, emailAddress, null).toString(); + } + } + } + + /** + * An asynchronous filter that performs search in a particular directory. + */ + private final class DirectoryFilter extends Filter { + private final DirectorySearchParams mParams; + private int mLimit; + + public DirectoryFilter(DirectorySearchParams params) { + this.mParams = params; + } + + public synchronized void setLimit(int limit) { + this.mLimit = limit; + } + + public synchronized int getLimit() { + return this.mLimit; + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + final FilterResults results = new FilterResults(); + if (!TextUtils.isEmpty(constraint)) { + final Cursor cursor = doQuery(constraint, getLimit(), mParams.directoryId); + if (cursor != null) { + results.values = cursor; + } + } + + // TODO: implement group feature + + return results; + } + + @Override + protected void publishResults(final CharSequence constraint, FilterResults results) { + final Cursor cursor = (Cursor) results.values; + mHandler.post(new Runnable() { + @Override + public void run() { + onDirectoryLoadFinished(constraint, mParams, cursor); + } + }); + results.count = getCount(); + } + } + + private final Context mContext; + private final ContentResolver mContentResolver; + private final LayoutInflater mInflater; + private final int mQueryType; + private Account mAccount; + private final int mPreferredMaxResultCount; + private final Handler mHandler = new Handler(); + + /** + * Each destination (an email address or a phone number) with a valid contactId is first + * inserted into {@link #mEntryMap} and grouped by the contactId. + * Destinations without valid contactId (possible if they aren't in local storage) are stored + * in {@link #mNonAggregatedEntries}. + * Duplicates are removed using {@link #mExistingDestinations}. + * + * After having all results from ContentResolver, all elements in mEntryMap are copied to + * mEntry, which will be used to find items in this Adapter. If the number of contacts in + * mEntries are less than mPreferredMaxResultCount, contacts in + * mNonAggregatedEntries are also used. + */ + private final HashMap<Integer, List<RecipientListEntry>> mEntryMap; + private final List<RecipientListEntry> mNonAggregatedEntries; + private final List<RecipientListEntry> mEntries; + private final Set<String> mExistingDestinations; + + /** + * Used to ignore asynchronous queries with a different constraint, which may appear when + * users type characters quickly. + */ + private CharSequence mCurrentConstraint; + + private final HandlerThread mPhotoHandlerThread; + private final Handler mPhotoHandler; + private final LruCache<Uri, byte[]> mPhotoCacheMap; + + /** + * Constructor for email queries. + */ + public BaseRecipientAdapter(Context context) { + this(context, QUERY_TYPE_EMAIL, DEFAULT_PREFERRED_MAX_RESULT_COUNT); + } + + public BaseRecipientAdapter(Context context, int queryType) { + this(context, queryType, DEFAULT_PREFERRED_MAX_RESULT_COUNT); + } + + public BaseRecipientAdapter(Context context, int queryType, int preferredMaxResultCount) { + mContext = context; + mContentResolver = context.getContentResolver(); + mInflater = LayoutInflater.from(context); + mQueryType = queryType; + mPreferredMaxResultCount = preferredMaxResultCount; + mEntryMap = new HashMap<Integer, List<RecipientListEntry>>(); + mNonAggregatedEntries = new ArrayList<RecipientListEntry>(); + mEntries = new ArrayList<RecipientListEntry>(); + mExistingDestinations = new HashSet<String>(); + mPhotoHandlerThread = new HandlerThread("photo_handler"); + mPhotoHandlerThread.start(); + mPhotoHandler = new Handler(mPhotoHandlerThread.getLooper()); + mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE); + } + + /** + * Set the account when known. Causes the search to prioritize contacts from that account. + */ + public void setAccount(Account account) { + mAccount = account; + } + + /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */ + @Override + public Filter getFilter() { + return new DefaultFilter(); + } + + /** + * Handles the result of the initial call, which brings back the list of directories as well + * as the search results for the local directories. + * + * Must be inside a default Looper thread to avoid synchronization problem. + */ + protected void onFirstDirectoryLoadFinished( + CharSequence constraint, Cursor directoryCursor, Cursor defaultDirectoryCursor) { + mCurrentConstraint = constraint; + + try { + final List<DirectorySearchParams> paramsList; + if (directoryCursor != null) { + paramsList = setupOtherDirectories(directoryCursor); + } else { + paramsList = null; + } + + int limit = 0; + + if (defaultDirectoryCursor != null) { + mEntryMap.clear(); + mNonAggregatedEntries.clear(); + mExistingDestinations.clear(); + putEntriesWithCursor(defaultDirectoryCursor, true); + constructEntryList(); + limit = mPreferredMaxResultCount - getCount(); + } + + if (limit > 0 && paramsList != null) { + searchOtherDirectories(constraint, paramsList, limit); + } + } finally { + if (directoryCursor != null) { + directoryCursor.close(); + } + if (defaultDirectoryCursor != null) { + defaultDirectoryCursor.close(); + } + } + } + + private List<DirectorySearchParams> setupOtherDirectories(Cursor directoryCursor) { + final PackageManager packageManager = mContext.getPackageManager(); + final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>(); + DirectorySearchParams preferredDirectory = null; + while (directoryCursor.moveToNext()) { + final long id = directoryCursor.getLong(DirectoryListQuery.ID); + + // Skip the local invisible directory, because the default directory already includes + // all local results. + if (id == Directory.LOCAL_INVISIBLE) { + continue; + } + + final DirectorySearchParams params = new DirectorySearchParams(); + final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); + final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); + params.directoryId = id; + params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); + params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); + params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); + if (packageName != null && resourceId != 0) { + try { + final Resources resources = + packageManager.getResourcesForApplication(packageName); + params.directoryType = resources.getString(resourceId); + if (params.directoryType == null) { + Log.e(TAG, "Cannot resolve directory name: " + + resourceId + "@" + packageName); + } + } catch (NameNotFoundException e) { + Log.e(TAG, "Cannot resolve directory name: " + + resourceId + "@" + packageName, e); + } + } + + // If an account has been provided and we found a directory that + // corresponds to that account, place that directory second, directly + // underneath the local contacts. + if (mAccount != null && mAccount.name.equals(params.accountName) && + mAccount.type.equals(params.accountType)) { + preferredDirectory = params; + } else { + paramsList.add(params); + } + } + + if (preferredDirectory != null) { + paramsList.add(1, preferredDirectory); + } + + return paramsList; + } + + /** + * Starts search in other directories + */ + private void searchOtherDirectories( + CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) { + final int count = paramsList.size(); + // Note: skipping the default partition (index 0), which has already been loaded + for (int i = 1; i < count; i++) { + final DirectorySearchParams params = paramsList.get(i); + params.constraint = constraint; + if (params.filter == null) { + params.filter = new DirectoryFilter(params); + } + params.filter.setLimit(limit); + params.filter.filter(constraint); + } + } + + /** Must be inside a default Looper thread to avoid synchronization problem. */ + public void onDirectoryLoadFinished( + CharSequence constraint, DirectorySearchParams params, Cursor cursor) { + if (cursor != null) { + try { + if (DEBUG) { + Log.v(TAG, "finished loading directory \"" + params.displayName + "\"" + + " with query " + constraint); + } + + // Check if the received result matches the current constraint + // If not - the user must have continued typing after the request was issued + final boolean usesSameConstraint; + usesSameConstraint = TextUtils.equals(constraint, mCurrentConstraint); + if (usesSameConstraint) { + putEntriesWithCursor(cursor, params.directoryId == Directory.DEFAULT); + constructEntryList(); + } + } finally { + cursor.close(); + } + } + } + + /** + * Stores each contact information to {@link #mEntryMap}. {@link #mEntries} isn't touched here. + * + * In order to make the new information available from outside Adapter, + * call {@link #constructEntryList()} after this method. + */ + private void putEntriesWithCursor(Cursor cursor, boolean validContactId) { + cursor.move(-1); + while (cursor.moveToNext()) { + final String displayName; + final String destination; + final int contactId; + final String thumbnailUriString; + if (mQueryType == QUERY_TYPE_EMAIL) { + displayName = cursor.getString(EmailQuery.NAME); + destination = cursor.getString(EmailQuery.ADDRESS); + contactId = cursor.getInt(EmailQuery.CONTACT_ID); + thumbnailUriString = cursor.getString(EmailQuery.PHOTO_THUMBNAIL_URI); + } else if (mQueryType == QUERY_TYPE_PHONE) { + displayName = cursor.getString(PhoneQuery.NAME); + destination = cursor.getString(PhoneQuery.NUMBER); + contactId = cursor.getInt(PhoneQuery.CONTACT_ID); + thumbnailUriString = cursor.getString(PhoneQuery.PHOTO_THUMBNAIL_URI); + } else { + throw new IndexOutOfBoundsException("Unexpected query type: " + mQueryType); + } + + // Note: At this point each entry doesn't contain have any photo (thus getPhotoBytes() + // returns null). + + if (mExistingDestinations.contains(destination)) { + continue; + } + mExistingDestinations.add(destination); + + if (!validContactId) { + mNonAggregatedEntries.add(RecipientListEntry.constructTopLevelEntry( + displayName, destination, contactId, thumbnailUriString)); + } else if (mEntryMap.containsKey(contactId)) { + // We already have a section for the person. + final List<RecipientListEntry> entryList = mEntryMap.get(contactId); + entryList.add(RecipientListEntry.constructSecondLevelEntry( + displayName, destination, contactId)); + } else { + final List<RecipientListEntry> entryList = new ArrayList<RecipientListEntry>(); + entryList.add(RecipientListEntry.constructTopLevelEntry( + displayName, destination, contactId, thumbnailUriString)); + mEntryMap.put(contactId, entryList); + } + } + } + + /** + * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to + * fetch a cached photo for each contact entry (other than separators), or request another + * thread to get one from directories. The thread ({@link #mPhotoHandlerThread}) will + * request {@link #notifyDataSetChanged()} after having the photo asynchronously. + */ + private void constructEntryList() { + mEntries.clear(); + int validEntryCount = 0; + for (Map.Entry<Integer, List<RecipientListEntry>> mapEntry : mEntryMap.entrySet()) { + final List<RecipientListEntry> entryList = mapEntry.getValue(); + final int size = entryList.size(); + for (int i = 0; i < size; i++) { + RecipientListEntry entry = entryList.get(i); + mEntries.add(entry); + tryFetchPhoto(entry); + validEntryCount++; + if (i < size - 1) { + mEntries.add(RecipientListEntry.SEP_WITHIN_GROUP); + } + } + mEntries.add(RecipientListEntry.SEP_NORMAL); + if (validEntryCount > mPreferredMaxResultCount) { + break; + } + } + if (validEntryCount <= mPreferredMaxResultCount) { + for (RecipientListEntry entry : mNonAggregatedEntries) { + if (validEntryCount > mPreferredMaxResultCount) { + break; + } + mEntries.add(entry); + tryFetchPhoto(entry); + + mEntries.add(RecipientListEntry.SEP_NORMAL); + validEntryCount++; + } + } + + // Remove last divider + if (mEntries.size() > 1) { + mEntries.remove(mEntries.size() - 1); + } + notifyDataSetChanged(); + } + + private void tryFetchPhoto(final RecipientListEntry entry) { + final Uri photoThumbnailUri = entry.getPhotoThumbnailUri(); + if (photoThumbnailUri != null) { + final byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri); + if (photoBytes != null) { + entry.setPhotoBytes(photoBytes); + // notifyDataSetChanged() should be called by a caller. + } else { + if (DEBUG) { + Log.d(TAG, "No photo cache for " + entry.getDisplayName() + + ". Fetch one asynchronously"); + } + fetchPhotoAsync(entry, photoThumbnailUri); + } + } + } + + private void fetchPhotoAsync(final RecipientListEntry entry, final Uri photoThumbnailUri) { + mPhotoHandler.post(new Runnable() { + @Override + public void run() { + final Cursor photoCursor = mContentResolver.query( + photoThumbnailUri, PhotoQuery.PROJECTION, null, null, null); + if (photoCursor != null) { + try { + if (photoCursor.moveToFirst()) { + final byte[] photoBytes = photoCursor.getBlob(PhotoQuery.PHOTO); + entry.setPhotoBytes(photoBytes); + + mHandler.post(new Runnable() { + @Override + public void run() { + mPhotoCacheMap.put(photoThumbnailUri, photoBytes); + notifyDataSetChanged(); + } + }); + } + } finally { + photoCursor.close(); + } + } + } + }); + } + + private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) { + final Cursor cursor; + if (mQueryType == QUERY_TYPE_EMAIL) { + final Uri.Builder builder = Email.CONTENT_FILTER_URI.buildUpon() + .appendPath(constraint.toString()) + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, + String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); + if (directoryId != null) { + builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, + String.valueOf(directoryId)); + } + cursor = mContentResolver.query( + builder.build(), EmailQuery.PROJECTION, null, null, null); + } else if (mQueryType == QUERY_TYPE_PHONE){ + final Uri.Builder builder = Phone.CONTENT_FILTER_URI.buildUpon() + .appendPath(constraint.toString()) + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, + String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); + if (directoryId != null) { + builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, + String.valueOf(directoryId)); + } + cursor = mContentResolver.query( + builder.build(), PhoneQuery.PROJECTION, null, null, null); + } else { + cursor = null; + } + return cursor; + } + + public void close() { + mEntryMap.clear(); + mNonAggregatedEntries.clear(); + mExistingDestinations.clear(); + mEntries.clear(); + mPhotoCacheMap.evictAll(); + if (!mPhotoHandlerThread.quit()) { + Log.w(TAG, "Failed to quit photo handler thread, ignoring it."); + } + } + + @Override + public int getCount() { + return mEntries.size(); + } + + @Override + public Object getItem(int position) { + return mEntries.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return RecipientListEntry.ENTRY_TYPE_SIZE; + } + + @Override + public int getItemViewType(int position) { + return mEntries.get(position).getEntryType(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final RecipientListEntry entry = mEntries.get(position); + switch (entry.getEntryType()) { + case RecipientListEntry.ENTRY_TYPE_SEP_NORMAL: { + return convertView != null ? convertView + : mInflater.inflate(getSeparatorLayout(), parent, false); + } + case RecipientListEntry.ENTRY_TYPE_SEP_WITHIN_GROUP: { + return convertView != null ? convertView + : mInflater.inflate(getSeparatorWithinGroupLayout(), parent, false); + } + default: { + String displayName = entry.getDisplayName(); + String emailAddress = entry.getDestination(); + if (TextUtils.isEmpty(displayName) + || TextUtils.equals(displayName, emailAddress)) { + displayName = emailAddress; + emailAddress = null; + } + + final View itemView = convertView != null ? convertView + : mInflater.inflate(getItemLayout(), parent, false); + final TextView displayNameView = + (TextView)itemView.findViewById(getDisplayNameId()); + final TextView emailAddressView = + (TextView)itemView.findViewById(getDestinationId()); + final ImageView imageView = (ImageView)itemView.findViewById(getPhotoId()); + displayNameView.setText(displayName); + if (!TextUtils.isEmpty(emailAddress)) { + emailAddressView.setText(emailAddress); + } + if (entry.isFirstLevel()) { + displayNameView.setVisibility(View.VISIBLE); + if (imageView != null) { + imageView.setVisibility(View.VISIBLE); + final byte[] photoBytes = entry.getPhotoBytes(); + if (photoBytes != null && imageView != null) { + final Bitmap photo = BitmapFactory.decodeByteArray( + photoBytes, 0, photoBytes.length); + imageView.setImageBitmap(photo); + } else { + imageView.setImageResource(getDefaultPhotoResource()); + } + } + } else { + displayNameView.setVisibility(View.GONE); + if (imageView != null) imageView.setVisibility(View.GONE); + } + return itemView; + } + } + } + + /** + * Returns a layout id for each item inside auto-complete list. + * + * Each View must contain two TextViews (for display name and destination) and one ImageView + * (for photo). Ids for those should be available via {@link #getDisplayNameId()}, + * {@link #getDestinationId()}, and {@link #getPhotoId()}. + */ + protected int getItemLayout() { + return R.layout.chips_recipient_dropdown_item; + } + + /** Returns a layout id for a separator dividing two person or groups. */ + protected int getSeparatorLayout() { + return R.layout.chips_separator; + } + + /** + * Returns a layout id for a separator dividing two destinations for a same person or group. + */ + protected int getSeparatorWithinGroupLayout() { + return R.layout.chips_separator_within_group; + } + + /** + * Returns a resource ID representing an image which should be shown when ther's no relevant + * photo is available. + */ + protected int getDefaultPhotoResource() { + return R.drawable.ic_contact_picture; + } + + /** + * Returns an id for TextView in an item View for showing a display name. In default + * {@link android.R.id#text1} is returned. + */ + protected int getDisplayNameId() { + return android.R.id.text1; + } + + /** + * Returns an id for TextView in an item View for showing a destination + * (an email address or a phone number). + * In default {@link android.R.id#text2} is returned. + */ + protected int getDestinationId() { + return android.R.id.text2; + } + + /** + * Returns an id for ImageView in an item View for showing photo image for a person. In default + * {@link android.R.id#icon} is returned. + */ + protected int getPhotoId() { + return android.R.id.icon; + } +} |