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