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