BaseRecipientAdapter.java revision 50429c51adbd79c5d7dd5beb2c267daf9465f20f
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.net.Uri;
27import android.os.Handler;
28import android.os.Message;
29import android.provider.ContactsContract;
30import android.provider.ContactsContract.Directory;
31import android.text.TextUtils;
32import android.text.util.Rfc822Token;
33import android.util.Log;
34import android.view.View;
35import android.view.ViewGroup;
36import android.widget.AutoCompleteTextView;
37import android.widget.BaseAdapter;
38import android.widget.Filter;
39import android.widget.Filterable;
40
41import com.android.ex.chips.DropdownChipLayouter.AdapterType;
42
43import java.util.ArrayList;
44import java.util.Collections;
45import java.util.HashSet;
46import java.util.LinkedHashMap;
47import java.util.List;
48import java.util.Map;
49import java.util.Set;
50
51/**
52 * Adapter for showing a recipient list.
53 */
54public class BaseRecipientAdapter extends BaseAdapter implements Filterable, AccountSpecifier,
55        PhotoManager.PhotoManagerCallback {
56    private static final String TAG = "BaseRecipientAdapter";
57
58    private static final boolean DEBUG = false;
59
60    /**
61     * The preferred number of results to be retrieved. This number may be
62     * exceeded if there are several directories configured, because we will use
63     * the same limit for all directories.
64     */
65    private static final int DEFAULT_PREFERRED_MAX_RESULT_COUNT = 10;
66
67    /**
68     * The number of extra entries requested to allow for duplicates. Duplicates
69     * are removed from the overall result.
70     */
71    static final int ALLOWANCE_FOR_DUPLICATES = 5;
72
73    // This is ContactsContract.PRIMARY_ACCOUNT_NAME. Available from ICS as hidden
74    static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account";
75    // This is ContactsContract.PRIMARY_ACCOUNT_TYPE. Available from ICS as hidden
76    static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account";
77
78    /**
79     * The "Waiting for more contacts" message will be displayed if search is not complete
80     * within this many milliseconds.
81     */
82    private static final int MESSAGE_SEARCH_PENDING_DELAY = 1000;
83    /** Used to prepare "Waiting for more contacts" message. */
84    private static final int MESSAGE_SEARCH_PENDING = 1;
85
86    public static final int QUERY_TYPE_EMAIL = 0;
87    public static final int QUERY_TYPE_PHONE = 1;
88
89    private final Queries.Query mQueryMode;
90    private final int mQueryType;
91
92    /**
93     * Model object for a {@link Directory} row.
94     */
95    public final static class DirectorySearchParams {
96        public long directoryId;
97        public String directoryType;
98        public String displayName;
99        public String accountName;
100        public String accountType;
101        public CharSequence constraint;
102        public DirectoryFilter filter;
103    }
104
105    protected static class DirectoryListQuery {
106
107        public static final Uri URI =
108                Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories");
109        public static final String[] PROJECTION = {
110            Directory._ID,              // 0
111            Directory.ACCOUNT_NAME,     // 1
112            Directory.ACCOUNT_TYPE,     // 2
113            Directory.DISPLAY_NAME,     // 3
114            Directory.PACKAGE_NAME,     // 4
115            Directory.TYPE_RESOURCE_ID, // 5
116        };
117
118        public static final int ID = 0;
119        public static final int ACCOUNT_NAME = 1;
120        public static final int ACCOUNT_TYPE = 2;
121        public static final int DISPLAY_NAME = 3;
122        public static final int PACKAGE_NAME = 4;
123        public static final int TYPE_RESOURCE_ID = 5;
124    }
125
126    /** Used to temporarily hold results in Cursor objects. */
127    protected static class TemporaryEntry {
128        public final String displayName;
129        public final String destination;
130        public final int destinationType;
131        public final String destinationLabel;
132        public final long contactId;
133        public final Long directoryId;
134        public final long dataId;
135        public final String thumbnailUriString;
136        public final int displayNameSource;
137        public final String lookupKey;
138
139        public TemporaryEntry(
140                String displayName,
141                String destination,
142                int destinationType,
143                String destinationLabel,
144                long contactId,
145                Long directoryId,
146                long dataId,
147                String thumbnailUriString,
148                int displayNameSource,
149                String lookupKey) {
150            this.displayName = displayName;
151            this.destination = destination;
152            this.destinationType = destinationType;
153            this.destinationLabel = destinationLabel;
154            this.contactId = contactId;
155            this.directoryId = directoryId;
156            this.dataId = dataId;
157            this.thumbnailUriString = thumbnailUriString;
158            this.displayNameSource = displayNameSource;
159            this.lookupKey = lookupKey;
160        }
161
162        public TemporaryEntry(Cursor cursor, Long directoryId) {
163            this.displayName = cursor.getString(Queries.Query.NAME);
164            this.destination = cursor.getString(Queries.Query.DESTINATION);
165            this.destinationType = cursor.getInt(Queries.Query.DESTINATION_TYPE);
166            this.destinationLabel = cursor.getString(Queries.Query.DESTINATION_LABEL);
167            this.contactId = cursor.getLong(Queries.Query.CONTACT_ID);
168            this.directoryId = directoryId;
169            this.dataId = cursor.getLong(Queries.Query.DATA_ID);
170            this.thumbnailUriString = cursor.getString(Queries.Query.PHOTO_THUMBNAIL_URI);
171            this.displayNameSource = cursor.getInt(Queries.Query.DISPLAY_NAME_SOURCE);
172            this.lookupKey = cursor.getString(Queries.Query.LOOKUP_KEY);
173        }
174    }
175
176    /**
177     * Used to pass results from {@link DefaultFilter#performFiltering(CharSequence)} to
178     * {@link DefaultFilter#publishResults(CharSequence, android.widget.Filter.FilterResults)}
179     */
180    private static class DefaultFilterResult {
181        public final List<RecipientEntry> entries;
182        public final LinkedHashMap<Long, List<RecipientEntry>> entryMap;
183        public final List<RecipientEntry> nonAggregatedEntries;
184        public final Set<String> existingDestinations;
185        public final List<DirectorySearchParams> paramsList;
186
187        public DefaultFilterResult(List<RecipientEntry> entries,
188                LinkedHashMap<Long, List<RecipientEntry>> entryMap,
189                List<RecipientEntry> nonAggregatedEntries,
190                Set<String> existingDestinations,
191                List<DirectorySearchParams> paramsList) {
192            this.entries = entries;
193            this.entryMap = entryMap;
194            this.nonAggregatedEntries = nonAggregatedEntries;
195            this.existingDestinations = existingDestinations;
196            this.paramsList = paramsList;
197        }
198    }
199
200    /**
201     * An asynchronous filter used for loading two data sets: email rows from the local
202     * contact provider and the list of {@link Directory}'s.
203     */
204    private final class DefaultFilter extends Filter {
205
206        @Override
207        protected FilterResults performFiltering(CharSequence constraint) {
208            if (DEBUG) {
209                Log.d(TAG, "start filtering. constraint: " + constraint + ", thread:"
210                        + Thread.currentThread());
211            }
212
213            final FilterResults results = new FilterResults();
214            Cursor defaultDirectoryCursor = null;
215            Cursor directoryCursor = null;
216
217            if (TextUtils.isEmpty(constraint)) {
218                clearTempEntries();
219                // Return empty results.
220                return results;
221            }
222
223            try {
224                defaultDirectoryCursor = doQuery(constraint, mPreferredMaxResultCount,
225                        null /* directoryId */);
226
227                if (defaultDirectoryCursor == null) {
228                    if (DEBUG) {
229                        Log.w(TAG, "null cursor returned for default Email filter query.");
230                    }
231                } else {
232                    // These variables will become mEntries, mEntryMap, mNonAggregatedEntries, and
233                    // mExistingDestinations. Here we shouldn't use those member variables directly
234                    // since this method is run outside the UI thread.
235                    final LinkedHashMap<Long, List<RecipientEntry>> entryMap =
236                            new LinkedHashMap<Long, List<RecipientEntry>>();
237                    final List<RecipientEntry> nonAggregatedEntries =
238                            new ArrayList<RecipientEntry>();
239                    final Set<String> existingDestinations = new HashSet<String>();
240
241                    while (defaultDirectoryCursor.moveToNext()) {
242                        // Note: At this point each entry doesn't contain any photo
243                        // (thus getPhotoBytes() returns null).
244                        putOneEntry(new TemporaryEntry(defaultDirectoryCursor,
245                                null /* directoryId */),
246                                true, entryMap, nonAggregatedEntries, existingDestinations);
247                    }
248
249                    // We'll copy this result to mEntry in publicResults() (run in the UX thread).
250                    final List<RecipientEntry> entries = constructEntryList(
251                            entryMap, nonAggregatedEntries);
252
253                    final List<DirectorySearchParams> paramsList =
254                            searchOtherDirectories(existingDestinations);
255
256                    results.values = new DefaultFilterResult(
257                            entries, entryMap, nonAggregatedEntries,
258                            existingDestinations, paramsList);
259                    results.count = 1;
260                }
261            } finally {
262                if (defaultDirectoryCursor != null) {
263                    defaultDirectoryCursor.close();
264                }
265                if (directoryCursor != null) {
266                    directoryCursor.close();
267                }
268            }
269            return results;
270        }
271
272        @Override
273        protected void publishResults(final CharSequence constraint, FilterResults results) {
274            mCurrentConstraint = constraint;
275
276            clearTempEntries();
277
278            if (results.values != null) {
279                DefaultFilterResult defaultFilterResult = (DefaultFilterResult) results.values;
280                mEntryMap = defaultFilterResult.entryMap;
281                mNonAggregatedEntries = defaultFilterResult.nonAggregatedEntries;
282                mExistingDestinations = defaultFilterResult.existingDestinations;
283
284                // If there are no local results, in the new result set, cache off what had been
285                // shown to the user for use until the first directory result is returned
286                if (defaultFilterResult.entries.size() == 0 &&
287                        defaultFilterResult.paramsList != null) {
288                    cacheCurrentEntries();
289                }
290
291                updateEntries(defaultFilterResult.entries);
292
293                // We need to search other remote directories, doing other Filter requests.
294                if (defaultFilterResult.paramsList != null) {
295                    final int limit = mPreferredMaxResultCount -
296                            defaultFilterResult.existingDestinations.size();
297                    startSearchOtherDirectories(constraint, defaultFilterResult.paramsList, limit);
298                }
299            } else {
300                updateEntries(Collections.<RecipientEntry>emptyList());
301            }
302        }
303
304        @Override
305        public CharSequence convertResultToString(Object resultValue) {
306            final RecipientEntry entry = (RecipientEntry)resultValue;
307            final String displayName = entry.getDisplayName();
308            final String emailAddress = entry.getDestination();
309            if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) {
310                 return emailAddress;
311            } else {
312                return new Rfc822Token(displayName, emailAddress, null).toString();
313            }
314        }
315    }
316
317    protected List<DirectorySearchParams> searchOtherDirectories(Set<String> existingDestinations) {
318        // After having local results, check the size of results. If the results are
319        // not enough, we search remote directories, which will take longer time.
320        final int limit = mPreferredMaxResultCount - existingDestinations.size();
321        if (limit > 0) {
322            if (DEBUG) {
323                Log.d(TAG, "More entries should be needed (current: "
324                        + existingDestinations.size()
325                        + ", remaining limit: " + limit + ") ");
326            }
327            final Cursor directoryCursor = mContentResolver.query(
328                    DirectoryListQuery.URI, DirectoryListQuery.PROJECTION,
329                    null, null, null);
330            return setupOtherDirectories(mContext, directoryCursor, mAccount);
331        } else {
332            // We don't need to search other directories.
333            return null;
334        }
335    }
336
337    /**
338     * An asynchronous filter that performs search in a particular directory.
339     */
340    protected class DirectoryFilter extends Filter {
341        private final DirectorySearchParams mParams;
342        private int mLimit;
343
344        public DirectoryFilter(DirectorySearchParams params) {
345            mParams = params;
346        }
347
348        public synchronized void setLimit(int limit) {
349            this.mLimit = limit;
350        }
351
352        public synchronized int getLimit() {
353            return this.mLimit;
354        }
355
356        @Override
357        protected FilterResults performFiltering(CharSequence constraint) {
358            if (DEBUG) {
359                Log.d(TAG, "DirectoryFilter#performFiltering. directoryId: " + mParams.directoryId
360                        + ", constraint: " + constraint + ", thread: " + Thread.currentThread());
361            }
362            final FilterResults results = new FilterResults();
363            results.values = null;
364            results.count = 0;
365
366            if (!TextUtils.isEmpty(constraint)) {
367                final ArrayList<TemporaryEntry> tempEntries = new ArrayList<TemporaryEntry>();
368
369                Cursor cursor = null;
370                try {
371                    // We don't want to pass this Cursor object to UI thread (b/5017608).
372                    // Assuming the result should contain fairly small results (at most ~10),
373                    // We just copy everything to local structure.
374                    cursor = doQuery(constraint, getLimit(), mParams.directoryId);
375
376                    if (cursor != null) {
377                        while (cursor.moveToNext()) {
378                            tempEntries.add(new TemporaryEntry(cursor, mParams.directoryId));
379                        }
380                    }
381                } finally {
382                    if (cursor != null) {
383                        cursor.close();
384                    }
385                }
386                if (!tempEntries.isEmpty()) {
387                    results.values = tempEntries;
388                    results.count = 1;
389                }
390            }
391
392            if (DEBUG) {
393                Log.v(TAG, "finished loading directory \"" + mParams.displayName + "\"" +
394                        " with query " + constraint);
395            }
396
397            return results;
398        }
399
400        @Override
401        protected void publishResults(final CharSequence constraint, FilterResults results) {
402            if (DEBUG) {
403                Log.d(TAG, "DirectoryFilter#publishResult. constraint: " + constraint
404                        + ", mCurrentConstraint: " + mCurrentConstraint);
405            }
406            mDelayedMessageHandler.removeDelayedLoadMessage();
407            // Check if the received result matches the current constraint
408            // If not - the user must have continued typing after the request was issued, which
409            // means several member variables (like mRemainingDirectoryLoad) are already
410            // overwritten so shouldn't be touched here anymore.
411            if (TextUtils.equals(constraint, mCurrentConstraint)) {
412                if (results.count > 0) {
413                    @SuppressWarnings("unchecked")
414                    final ArrayList<TemporaryEntry> tempEntries =
415                            (ArrayList<TemporaryEntry>) results.values;
416
417                    for (TemporaryEntry tempEntry : tempEntries) {
418                        putOneEntry(tempEntry, mParams.directoryId == Directory.DEFAULT);
419                    }
420                }
421
422                // If there are remaining directories, set up delayed message again.
423                mRemainingDirectoryCount--;
424                if (mRemainingDirectoryCount > 0) {
425                    if (DEBUG) {
426                        Log.d(TAG, "Resend delayed load message. Current mRemainingDirectoryLoad: "
427                                + mRemainingDirectoryCount);
428                    }
429                    mDelayedMessageHandler.sendDelayedLoadMessage();
430                }
431
432                // If this directory result has some items, or there are no more directories that
433                // we are waiting for, clear the temp results
434                if (results.count > 0 || mRemainingDirectoryCount == 0) {
435                    // Clear the temp entries
436                    clearTempEntries();
437                }
438            }
439
440            // Show the list again without "waiting" message.
441            updateEntries(constructEntryList());
442        }
443    }
444
445    private final Context mContext;
446    private final ContentResolver mContentResolver;
447    private Account mAccount;
448    protected final int mPreferredMaxResultCount;
449    private DropdownChipLayouter mDropdownChipLayouter;
450
451    /**
452     * {@link #mEntries} is responsible for showing every result for this Adapter. To
453     * construct it, we use {@link #mEntryMap}, {@link #mNonAggregatedEntries}, and
454     * {@link #mExistingDestinations}.
455     *
456     * First, each destination (an email address or a phone number) with a valid contactId is
457     * inserted into {@link #mEntryMap} and grouped by the contactId. Destinations without valid
458     * contactId (possible if they aren't in local storage) are stored in
459     * {@link #mNonAggregatedEntries}.
460     * Duplicates are removed using {@link #mExistingDestinations}.
461     *
462     * After having all results from Cursor objects, all destinations in mEntryMap are copied to
463     * {@link #mEntries}. If the number of destinations is not enough (i.e. less than
464     * {@link #mPreferredMaxResultCount}), destinations in mNonAggregatedEntries are also used.
465     *
466     * These variables are only used in UI thread, thus should not be touched in
467     * performFiltering() methods.
468     */
469    private LinkedHashMap<Long, List<RecipientEntry>> mEntryMap;
470    private List<RecipientEntry> mNonAggregatedEntries;
471    private Set<String> mExistingDestinations;
472    /** Note: use {@link #updateEntries(List)} to update this variable. */
473    private List<RecipientEntry> mEntries;
474    private List<RecipientEntry> mTempEntries;
475
476    /** The number of directories this adapter is waiting for results. */
477    private int mRemainingDirectoryCount;
478
479    /**
480     * Used to ignore asynchronous queries with a different constraint, which may happen when
481     * users type characters quickly.
482     */
483    protected CharSequence mCurrentConstraint;
484
485    /**
486     * Performs all photo querying as well as caching for repeated lookups.
487     */
488    private PhotoManager mPhotoManager;
489
490    /**
491     * Handler specific for maintaining "Waiting for more contacts" message, which will be shown
492     * when:
493     * - there are directories to be searched
494     * - results from directories are slow to come
495     */
496    private final class DelayedMessageHandler extends Handler {
497        @Override
498        public void handleMessage(Message msg) {
499            if (mRemainingDirectoryCount > 0) {
500                updateEntries(constructEntryList());
501            }
502        }
503
504        public void sendDelayedLoadMessage() {
505            sendMessageDelayed(obtainMessage(MESSAGE_SEARCH_PENDING, 0, 0, null),
506                    MESSAGE_SEARCH_PENDING_DELAY);
507        }
508
509        public void removeDelayedLoadMessage() {
510            removeMessages(MESSAGE_SEARCH_PENDING);
511        }
512    }
513
514    private final DelayedMessageHandler mDelayedMessageHandler = new DelayedMessageHandler();
515
516    private EntriesUpdatedObserver mEntriesUpdatedObserver;
517
518    /**
519     * Constructor for email queries.
520     */
521    public BaseRecipientAdapter(Context context) {
522        this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, QUERY_TYPE_EMAIL);
523    }
524
525    public BaseRecipientAdapter(Context context, int preferredMaxResultCount) {
526        this(context, preferredMaxResultCount, QUERY_TYPE_EMAIL);
527    }
528
529    public BaseRecipientAdapter(int queryMode, Context context) {
530        this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, queryMode);
531    }
532
533    public BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount) {
534        this(context, preferredMaxResultCount, queryMode);
535    }
536
537    public BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode) {
538        mContext = context;
539        mContentResolver = context.getContentResolver();
540        mPreferredMaxResultCount = preferredMaxResultCount;
541        mPhotoManager = new DefaultPhotoManager(mContentResolver);
542        mQueryType = queryMode;
543
544        if (queryMode == QUERY_TYPE_EMAIL) {
545            mQueryMode = Queries.EMAIL;
546        } else if (queryMode == QUERY_TYPE_PHONE) {
547            mQueryMode = Queries.PHONE;
548        } else {
549            mQueryMode = Queries.EMAIL;
550            Log.e(TAG, "Unsupported query type: " + queryMode);
551        }
552    }
553
554    public Context getContext() {
555        return mContext;
556    }
557
558    public int getQueryType() {
559        return mQueryType;
560    }
561
562    public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
563        mDropdownChipLayouter = dropdownChipLayouter;
564        mDropdownChipLayouter.setQuery(mQueryMode);
565    }
566
567    public DropdownChipLayouter getDropdownChipLayouter() {
568        return mDropdownChipLayouter;
569    }
570
571    /**
572     * Enables overriding the default photo manager that is used.
573     */
574    public void setPhotoManager(PhotoManager photoManager) {
575        mPhotoManager = photoManager;
576    }
577
578    public PhotoManager getPhotoManager() {
579        return mPhotoManager;
580    }
581
582    /**
583     * If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter}
584     * instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when
585     * clicking on a chip. Default implementation returns {@code false}.
586     */
587    public boolean forceShowAddress() {
588        return false;
589    }
590
591    /**
592     * Used to replace email addresses with chips. Default behavior
593     * queries the ContactsProvider for contact information about the contact.
594     * Derived classes should override this method if they wish to use a
595     * new data source.
596     * @param inAddresses addresses to query
597     * @param callback callback to return results in case of success or failure
598     */
599    public void getMatchingRecipients(ArrayList<String> inAddresses,
600            RecipientAlternatesAdapter.RecipientMatchCallback callback) {
601        RecipientAlternatesAdapter.getMatchingRecipients(
602                getContext(), this, inAddresses, getAccount(), callback);
603    }
604
605    /**
606     * Set the account when known. Causes the search to prioritize contacts from that account.
607     */
608    @Override
609    public void setAccount(Account account) {
610        mAccount = account;
611    }
612
613    /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */
614    @Override
615    public Filter getFilter() {
616        return new DefaultFilter();
617    }
618
619    /**
620     * An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows
621     * additional sources of contacts to be considered as matching recipients.
622     * @param addresses A set of addresses to be matched
623     * @return A list of matches or null if none found
624     */
625    public Map<String, RecipientEntry> getMatchingRecipients(Set<String> addresses) {
626        return null;
627    }
628
629    public static List<DirectorySearchParams> setupOtherDirectories(Context context,
630            Cursor directoryCursor, Account account) {
631        final PackageManager packageManager = context.getPackageManager();
632        final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>();
633        DirectorySearchParams preferredDirectory = null;
634        while (directoryCursor.moveToNext()) {
635            final long id = directoryCursor.getLong(DirectoryListQuery.ID);
636
637            // Skip the local invisible directory, because the default directory already includes
638            // all local results.
639            if (id == Directory.LOCAL_INVISIBLE) {
640                continue;
641            }
642
643            final DirectorySearchParams params = new DirectorySearchParams();
644            final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME);
645            final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID);
646            params.directoryId = id;
647            params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME);
648            params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME);
649            params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE);
650            if (packageName != null && resourceId != 0) {
651                try {
652                    final Resources resources =
653                            packageManager.getResourcesForApplication(packageName);
654                    params.directoryType = resources.getString(resourceId);
655                    if (params.directoryType == null) {
656                        Log.e(TAG, "Cannot resolve directory name: "
657                                + resourceId + "@" + packageName);
658                    }
659                } catch (NameNotFoundException e) {
660                    Log.e(TAG, "Cannot resolve directory name: "
661                            + resourceId + "@" + packageName, e);
662                }
663            }
664
665            // If an account has been provided and we found a directory that
666            // corresponds to that account, place that directory second, directly
667            // underneath the local contacts.
668            if (account != null && account.name.equals(params.accountName) &&
669                    account.type.equals(params.accountType)) {
670                preferredDirectory = params;
671            } else {
672                paramsList.add(params);
673            }
674        }
675
676        if (preferredDirectory != null) {
677            paramsList.add(1, preferredDirectory);
678        }
679
680        return paramsList;
681    }
682
683    /**
684     * Starts search in other directories using {@link Filter}. Results will be handled in
685     * {@link DirectoryFilter}.
686     */
687    protected void startSearchOtherDirectories(
688            CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) {
689        final int count = paramsList.size();
690        // Note: skipping the default partition (index 0), which has already been loaded
691        for (int i = 1; i < count; i++) {
692            final DirectorySearchParams params = paramsList.get(i);
693            params.constraint = constraint;
694            if (params.filter == null) {
695                params.filter = new DirectoryFilter(params);
696            }
697            params.filter.setLimit(limit);
698            params.filter.filter(constraint);
699        }
700
701        // Directory search started. We may show "waiting" message if directory results are slow
702        // enough.
703        mRemainingDirectoryCount = count - 1;
704        mDelayedMessageHandler.sendDelayedLoadMessage();
705    }
706
707    /**
708     * Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter}
709     * wants to add an additional entry to the results. Derived classes should override
710     * this method if they are not using the default data structures provided by
711     * {@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their
712     * own data structures to store and collate data.
713     * @param entry the entry being added
714     * @param isAggregatedEntry
715     */
716    protected void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry) {
717        putOneEntry(entry, isAggregatedEntry,
718                mEntryMap, mNonAggregatedEntries, mExistingDestinations);
719    }
720
721    private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry,
722            LinkedHashMap<Long, List<RecipientEntry>> entryMap,
723            List<RecipientEntry> nonAggregatedEntries,
724            Set<String> existingDestinations) {
725        if (existingDestinations.contains(entry.destination)) {
726            return;
727        }
728
729        existingDestinations.add(entry.destination);
730
731        if (!isAggregatedEntry) {
732            nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry(
733                    entry.displayName,
734                    entry.displayNameSource,
735                    entry.destination, entry.destinationType, entry.destinationLabel,
736                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
737                    true, entry.lookupKey));
738        } else if (entryMap.containsKey(entry.contactId)) {
739            // We already have a section for the person.
740            final List<RecipientEntry> entryList = entryMap.get(entry.contactId);
741            entryList.add(RecipientEntry.constructSecondLevelEntry(
742                    entry.displayName,
743                    entry.displayNameSource,
744                    entry.destination, entry.destinationType, entry.destinationLabel,
745                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
746                    true, entry.lookupKey));
747        } else {
748            final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>();
749            entryList.add(RecipientEntry.constructTopLevelEntry(
750                    entry.displayName,
751                    entry.displayNameSource,
752                    entry.destination, entry.destinationType, entry.destinationLabel,
753                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
754                    true, entry.lookupKey));
755            entryMap.put(entry.contactId, entryList);
756        }
757    }
758
759    /**
760     * Returns the actual list to use for this Adapter. Derived classes
761     * should override this method if overriding how the adapter stores and collates
762     * data.
763     */
764    protected List<RecipientEntry> constructEntryList() {
765        return constructEntryList(mEntryMap, mNonAggregatedEntries);
766    }
767
768    /**
769     * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to
770     * fetch a cached photo for each contact entry (other than separators), or request another
771     * thread to get one from directories.
772     */
773    private List<RecipientEntry> constructEntryList(
774            LinkedHashMap<Long, List<RecipientEntry>> entryMap,
775            List<RecipientEntry> nonAggregatedEntries) {
776        final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
777        int validEntryCount = 0;
778        for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) {
779            final List<RecipientEntry> entryList = mapEntry.getValue();
780            final int size = entryList.size();
781            for (int i = 0; i < size; i++) {
782                RecipientEntry entry = entryList.get(i);
783                entries.add(entry);
784                mPhotoManager.populatePhotoBytesAsync(entry, this);
785                validEntryCount++;
786            }
787            if (validEntryCount > mPreferredMaxResultCount) {
788                break;
789            }
790        }
791        if (validEntryCount <= mPreferredMaxResultCount) {
792            for (RecipientEntry entry : nonAggregatedEntries) {
793                if (validEntryCount > mPreferredMaxResultCount) {
794                    break;
795                }
796                entries.add(entry);
797                mPhotoManager.populatePhotoBytesAsync(entry, this);
798                validEntryCount++;
799            }
800        }
801
802        return entries;
803    }
804
805
806    public interface EntriesUpdatedObserver {
807        public void onChanged(List<RecipientEntry> entries);
808    }
809
810    public void registerUpdateObserver(EntriesUpdatedObserver observer) {
811        mEntriesUpdatedObserver = observer;
812    }
813
814    /** Resets {@link #mEntries} and notify the event to its parent ListView. */
815    protected void updateEntries(List<RecipientEntry> newEntries) {
816        mEntries = newEntries;
817        mEntriesUpdatedObserver.onChanged(newEntries);
818        notifyDataSetChanged();
819    }
820
821    protected void cacheCurrentEntries() {
822        mTempEntries = mEntries;
823    }
824
825    protected void clearTempEntries() {
826        mTempEntries = null;
827    }
828
829    protected List<RecipientEntry> getEntries() {
830        return mTempEntries != null ? mTempEntries : mEntries;
831    }
832
833    protected void fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb) {
834        mPhotoManager.populatePhotoBytesAsync(entry, cb);
835    }
836
837    private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) {
838        final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon()
839                .appendPath(constraint.toString())
840                .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
841                        String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES));
842        if (directoryId != null) {
843            builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
844                    String.valueOf(directoryId));
845        }
846        if (mAccount != null) {
847            builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name);
848            builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type);
849        }
850        final long start = System.currentTimeMillis();
851        final Cursor cursor = mContentResolver.query(
852                builder.build(), mQueryMode.getProjection(), null, null, null);
853        final long end = System.currentTimeMillis();
854        if (DEBUG) {
855            Log.d(TAG, "Time for autocomplete (query: " + constraint
856                    + ", directoryId: " + directoryId + ", num_of_results: "
857                    + (cursor != null ? cursor.getCount() : "null") + "): "
858                    + (end - start) + " ms");
859        }
860        return cursor;
861    }
862
863    // TODO: This won't be used at all. We should find better way to quit the thread..
864    /*public void close() {
865        mEntries = null;
866        mPhotoCacheMap.evictAll();
867        if (!sPhotoHandlerThread.quit()) {
868            Log.w(TAG, "Failed to quit photo handler thread, ignoring it.");
869        }
870    }*/
871
872    @Override
873    public int getCount() {
874        final List<RecipientEntry> entries = getEntries();
875        return entries != null ? entries.size() : 0;
876    }
877
878    @Override
879    public RecipientEntry getItem(int position) {
880        return getEntries().get(position);
881    }
882
883    @Override
884    public long getItemId(int position) {
885        return position;
886    }
887
888    @Override
889    public int getViewTypeCount() {
890        return RecipientEntry.ENTRY_TYPE_SIZE;
891    }
892
893    @Override
894    public int getItemViewType(int position) {
895        return getEntries().get(position).getEntryType();
896    }
897
898    @Override
899    public boolean isEnabled(int position) {
900        return getEntries().get(position).isSelectable();
901    }
902
903    @Override
904    public View getView(int position, View convertView, ViewGroup parent) {
905        final RecipientEntry entry = getEntries().get(position);
906
907        final String constraint = mCurrentConstraint == null ? null :
908                mCurrentConstraint.toString();
909
910        return mDropdownChipLayouter.bindView(convertView, parent, entry, position,
911                AdapterType.BASE_RECIPIENT, constraint);
912    }
913
914    public Account getAccount() {
915        return mAccount;
916    }
917
918    @Override
919    public void onPhotoBytesPopulated() {
920        // Default implementation does nothing
921    }
922
923    @Override
924    public void onPhotoBytesAsynchronouslyPopulated() {
925        notifyDataSetChanged();
926    }
927
928    @Override
929    public void onPhotoBytesAsyncLoadFailed() {
930        // Default implementation does nothing
931    }
932}
933