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