BaseRecipientAdapter.java revision efcac0cbb3efc645cd6cf1cb1e2431e1bd2b2d2a
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
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 RecipientListEntry entry = (RecipientListEntry)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 HashMap<Integer, List<RecipientListEntry>> mEntryMap;
278    private final List<RecipientListEntry> mNonAggregatedEntries;
279    private final List<RecipientListEntry> 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 HashMap<Integer, List<RecipientListEntry>>();
310        mNonAggregatedEntries = new ArrayList<RecipientListEntry>();
311        mEntries = new ArrayList<RecipientListEntry>();
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(RecipientListEntry.constructTopLevelEntry(
506                        displayName, destination, contactId, thumbnailUriString));
507            } else if (mEntryMap.containsKey(contactId)) {
508                // We already have a section for the person.
509                final List<RecipientListEntry> entryList = mEntryMap.get(contactId);
510                entryList.add(RecipientListEntry.constructSecondLevelEntry(
511                        displayName, destination, contactId));
512            } else {
513                final List<RecipientListEntry> entryList = new ArrayList<RecipientListEntry>();
514                entryList.add(RecipientListEntry.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<RecipientListEntry>> mapEntry : mEntryMap.entrySet()) {
531            final List<RecipientListEntry> entryList = mapEntry.getValue();
532            final int size = entryList.size();
533            for (int i = 0; i < size; i++) {
534                RecipientListEntry entry = entryList.get(i);
535                mEntries.add(entry);
536                tryFetchPhoto(entry);
537                validEntryCount++;
538                if (i < size - 1) {
539                    mEntries.add(RecipientListEntry.SEP_WITHIN_GROUP);
540                }
541            }
542            mEntries.add(RecipientListEntry.SEP_NORMAL);
543            if (validEntryCount > mPreferredMaxResultCount) {
544                break;
545            }
546        }
547        if (validEntryCount <= mPreferredMaxResultCount) {
548            for (RecipientListEntry entry : mNonAggregatedEntries) {
549                if (validEntryCount > mPreferredMaxResultCount) {
550                    break;
551                }
552                mEntries.add(entry);
553                tryFetchPhoto(entry);
554
555                mEntries.add(RecipientListEntry.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 RecipientListEntry 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 RecipientListEntry 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 RecipientListEntry.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 RecipientListEntry entry = mEntries.get(position);
681        switch (entry.getEntryType()) {
682            case RecipientListEntry.ENTRY_TYPE_SEP_NORMAL: {
683                return convertView != null ? convertView
684                        : mInflater.inflate(getSeparatorLayout(), parent, false);
685            }
686            case RecipientListEntry.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