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