BaseRecipientAdapter.java revision 35e82d4f9522906f7953667cf5c5f8137ec2f5ac
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, a null thumbnail uri is ignored when trying to query for a photo.
584     * Derived classes should only return true if a {@link com.android.ex.chips.PhotoManager}
585     * is used that does not rely on thumbnail uris. Default implementation returns {@code false}.
586     * @return
587     */
588    public boolean ignoreNullThumbnailUri() {
589        return false;
590    }
591
592    /**
593     * If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter}
594     * instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when
595     * clicking on a chip. Default implementation returns {@code false}.
596     */
597    public boolean forceShowAddress() {
598        return false;
599    }
600
601    /**
602     * Used to replace email addresses with chips. Default behavior
603     * queries the ContactsProvider for contact information about the contact.
604     * Derived classes should override this method if they wish to use a
605     * new data source.
606     * @param inAddresses addresses to query
607     * @param callback callback to return results in case of success or failure
608     */
609    public void getMatchingRecipients(ArrayList<String> inAddresses,
610            RecipientAlternatesAdapter.RecipientMatchCallback callback) {
611        RecipientAlternatesAdapter.getMatchingRecipients(
612                getContext(), this, inAddresses, getAccount(), callback);
613    }
614
615    /**
616     * Set the account when known. Causes the search to prioritize contacts from that account.
617     */
618    @Override
619    public void setAccount(Account account) {
620        mAccount = account;
621    }
622
623    /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */
624    @Override
625    public Filter getFilter() {
626        return new DefaultFilter();
627    }
628
629    /**
630     * An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows
631     * additional sources of contacts to be considered as matching recipients.
632     * @param addresses A set of addresses to be matched
633     * @return A list of matches or null if none found
634     */
635    public Map<String, RecipientEntry> getMatchingRecipients(Set<String> addresses) {
636        return null;
637    }
638
639    public static List<DirectorySearchParams> setupOtherDirectories(Context context,
640            Cursor directoryCursor, Account account) {
641        final PackageManager packageManager = context.getPackageManager();
642        final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>();
643        DirectorySearchParams preferredDirectory = null;
644        while (directoryCursor.moveToNext()) {
645            final long id = directoryCursor.getLong(DirectoryListQuery.ID);
646
647            // Skip the local invisible directory, because the default directory already includes
648            // all local results.
649            if (id == Directory.LOCAL_INVISIBLE) {
650                continue;
651            }
652
653            final DirectorySearchParams params = new DirectorySearchParams();
654            final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME);
655            final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID);
656            params.directoryId = id;
657            params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME);
658            params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME);
659            params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE);
660            if (packageName != null && resourceId != 0) {
661                try {
662                    final Resources resources =
663                            packageManager.getResourcesForApplication(packageName);
664                    params.directoryType = resources.getString(resourceId);
665                    if (params.directoryType == null) {
666                        Log.e(TAG, "Cannot resolve directory name: "
667                                + resourceId + "@" + packageName);
668                    }
669                } catch (NameNotFoundException e) {
670                    Log.e(TAG, "Cannot resolve directory name: "
671                            + resourceId + "@" + packageName, e);
672                }
673            }
674
675            // If an account has been provided and we found a directory that
676            // corresponds to that account, place that directory second, directly
677            // underneath the local contacts.
678            if (account != null && account.name.equals(params.accountName) &&
679                    account.type.equals(params.accountType)) {
680                preferredDirectory = params;
681            } else {
682                paramsList.add(params);
683            }
684        }
685
686        if (preferredDirectory != null) {
687            paramsList.add(1, preferredDirectory);
688        }
689
690        return paramsList;
691    }
692
693    /**
694     * Starts search in other directories using {@link Filter}. Results will be handled in
695     * {@link DirectoryFilter}.
696     */
697    protected void startSearchOtherDirectories(
698            CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) {
699        final int count = paramsList.size();
700        // Note: skipping the default partition (index 0), which has already been loaded
701        for (int i = 1; i < count; i++) {
702            final DirectorySearchParams params = paramsList.get(i);
703            params.constraint = constraint;
704            if (params.filter == null) {
705                params.filter = new DirectoryFilter(params);
706            }
707            params.filter.setLimit(limit);
708            params.filter.filter(constraint);
709        }
710
711        // Directory search started. We may show "waiting" message if directory results are slow
712        // enough.
713        mRemainingDirectoryCount = count - 1;
714        mDelayedMessageHandler.sendDelayedLoadMessage();
715    }
716
717    /**
718     * Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter}
719     * wants to add an additional entry to the results. Derived classes should override
720     * this method if they are not using the default data structures provided by
721     * {@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their
722     * own data structures to store and collate data.
723     * @param entry the entry being added
724     * @param isAggregatedEntry
725     */
726    protected void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry) {
727        putOneEntry(entry, isAggregatedEntry,
728                mEntryMap, mNonAggregatedEntries, mExistingDestinations);
729    }
730
731    private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry,
732            LinkedHashMap<Long, List<RecipientEntry>> entryMap,
733            List<RecipientEntry> nonAggregatedEntries,
734            Set<String> existingDestinations) {
735        if (existingDestinations.contains(entry.destination)) {
736            return;
737        }
738
739        existingDestinations.add(entry.destination);
740
741        if (!isAggregatedEntry) {
742            nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry(
743                    entry.displayName,
744                    entry.displayNameSource,
745                    entry.destination, entry.destinationType, entry.destinationLabel,
746                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
747                    true, entry.lookupKey));
748        } else if (entryMap.containsKey(entry.contactId)) {
749            // We already have a section for the person.
750            final List<RecipientEntry> entryList = entryMap.get(entry.contactId);
751            entryList.add(RecipientEntry.constructSecondLevelEntry(
752                    entry.displayName,
753                    entry.displayNameSource,
754                    entry.destination, entry.destinationType, entry.destinationLabel,
755                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
756                    true, entry.lookupKey));
757        } else {
758            final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>();
759            entryList.add(RecipientEntry.constructTopLevelEntry(
760                    entry.displayName,
761                    entry.displayNameSource,
762                    entry.destination, entry.destinationType, entry.destinationLabel,
763                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
764                    true, entry.lookupKey));
765            entryMap.put(entry.contactId, entryList);
766        }
767    }
768
769    /**
770     * Returns the actual list to use for this Adapter. Derived classes
771     * should override this method if overriding how the adapter stores and collates
772     * data.
773     */
774    protected List<RecipientEntry> constructEntryList() {
775        return constructEntryList(mEntryMap, mNonAggregatedEntries);
776    }
777
778    /**
779     * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to
780     * fetch a cached photo for each contact entry (other than separators), or request another
781     * thread to get one from directories.
782     */
783    private List<RecipientEntry> constructEntryList(
784            LinkedHashMap<Long, List<RecipientEntry>> entryMap,
785            List<RecipientEntry> nonAggregatedEntries) {
786        final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
787        int validEntryCount = 0;
788        for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) {
789            final List<RecipientEntry> entryList = mapEntry.getValue();
790            final int size = entryList.size();
791            for (int i = 0; i < size; i++) {
792                RecipientEntry entry = entryList.get(i);
793                entries.add(entry);
794                mPhotoManager.populatePhotoBytesAsync(entry, this);
795                validEntryCount++;
796            }
797            if (validEntryCount > mPreferredMaxResultCount) {
798                break;
799            }
800        }
801        if (validEntryCount <= mPreferredMaxResultCount) {
802            for (RecipientEntry entry : nonAggregatedEntries) {
803                if (validEntryCount > mPreferredMaxResultCount) {
804                    break;
805                }
806                entries.add(entry);
807                mPhotoManager.populatePhotoBytesAsync(entry, this);
808                validEntryCount++;
809            }
810        }
811
812        return entries;
813    }
814
815
816    public interface EntriesUpdatedObserver {
817        public void onChanged(List<RecipientEntry> entries);
818    }
819
820    public void registerUpdateObserver(EntriesUpdatedObserver observer) {
821        mEntriesUpdatedObserver = observer;
822    }
823
824    /** Resets {@link #mEntries} and notify the event to its parent ListView. */
825    protected void updateEntries(List<RecipientEntry> newEntries) {
826        mEntries = newEntries;
827        mEntriesUpdatedObserver.onChanged(newEntries);
828        notifyDataSetChanged();
829    }
830
831    protected void cacheCurrentEntries() {
832        mTempEntries = mEntries;
833    }
834
835    protected void clearTempEntries() {
836        mTempEntries = null;
837    }
838
839    protected List<RecipientEntry> getEntries() {
840        return mTempEntries != null ? mTempEntries : mEntries;
841    }
842
843    protected void fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb) {
844        mPhotoManager.populatePhotoBytesAsync(entry, cb);
845    }
846
847    private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) {
848        final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon()
849                .appendPath(constraint.toString())
850                .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
851                        String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES));
852        if (directoryId != null) {
853            builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
854                    String.valueOf(directoryId));
855        }
856        if (mAccount != null) {
857            builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name);
858            builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type);
859        }
860        final long start = System.currentTimeMillis();
861        final Cursor cursor = mContentResolver.query(
862                builder.build(), mQueryMode.getProjection(), null, null, null);
863        final long end = System.currentTimeMillis();
864        if (DEBUG) {
865            Log.d(TAG, "Time for autocomplete (query: " + constraint
866                    + ", directoryId: " + directoryId + ", num_of_results: "
867                    + (cursor != null ? cursor.getCount() : "null") + "): "
868                    + (end - start) + " ms");
869        }
870        return cursor;
871    }
872
873    // TODO: This won't be used at all. We should find better way to quit the thread..
874    /*public void close() {
875        mEntries = null;
876        mPhotoCacheMap.evictAll();
877        if (!sPhotoHandlerThread.quit()) {
878            Log.w(TAG, "Failed to quit photo handler thread, ignoring it.");
879        }
880    }*/
881
882    @Override
883    public int getCount() {
884        final List<RecipientEntry> entries = getEntries();
885        return entries != null ? entries.size() : 0;
886    }
887
888    @Override
889    public RecipientEntry getItem(int position) {
890        return getEntries().get(position);
891    }
892
893    @Override
894    public long getItemId(int position) {
895        return position;
896    }
897
898    @Override
899    public int getViewTypeCount() {
900        return RecipientEntry.ENTRY_TYPE_SIZE;
901    }
902
903    @Override
904    public int getItemViewType(int position) {
905        return getEntries().get(position).getEntryType();
906    }
907
908    @Override
909    public boolean isEnabled(int position) {
910        return getEntries().get(position).isSelectable();
911    }
912
913    @Override
914    public View getView(int position, View convertView, ViewGroup parent) {
915        final RecipientEntry entry = getEntries().get(position);
916
917        final String constraint = mCurrentConstraint == null ? null :
918                mCurrentConstraint.toString();
919
920        return mDropdownChipLayouter.bindView(convertView, parent, entry, position,
921                AdapterType.BASE_RECIPIENT, constraint);
922    }
923
924    public Account getAccount() {
925        return mAccount;
926    }
927
928    @Override
929    public void onPhotoBytesAsynchronouslyPopulated() {
930        notifyDataSetChanged();
931    }
932}
933