BaseRecipientAdapter.java revision 0437e862005912a1e5bf054769f23ed9cc69f811
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 = entries.size();
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                cacheCurrentEntriesIfNeeded(defaultFilterResult.entries.size(),
285                        defaultFilterResult.paramsList == null ? 0 :
286                                defaultFilterResult.paramsList.size());
287
288                updateEntries(defaultFilterResult.entries);
289
290                // We need to search other remote directories, doing other Filter requests.
291                if (defaultFilterResult.paramsList != null) {
292                    final int limit = mPreferredMaxResultCount -
293                            defaultFilterResult.existingDestinations.size();
294                    startSearchOtherDirectories(constraint, defaultFilterResult.paramsList, limit);
295                }
296            } else {
297                updateEntries(Collections.<RecipientEntry>emptyList());
298            }
299        }
300
301        @Override
302        public CharSequence convertResultToString(Object resultValue) {
303            final RecipientEntry entry = (RecipientEntry)resultValue;
304            final String displayName = entry.getDisplayName();
305            final String emailAddress = entry.getDestination();
306            if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) {
307                 return emailAddress;
308            } else {
309                return new Rfc822Token(displayName, emailAddress, null).toString();
310            }
311        }
312    }
313
314    protected List<DirectorySearchParams> searchOtherDirectories(Set<String> existingDestinations) {
315        // After having local results, check the size of results. If the results are
316        // not enough, we search remote directories, which will take longer time.
317        final int limit = mPreferredMaxResultCount - existingDestinations.size();
318        if (limit > 0) {
319            if (DEBUG) {
320                Log.d(TAG, "More entries should be needed (current: "
321                        + existingDestinations.size()
322                        + ", remaining limit: " + limit + ") ");
323            }
324            Cursor directoryCursor = null;
325            try {
326                directoryCursor = mContentResolver.query(
327                        DirectoryListQuery.URI, DirectoryListQuery.PROJECTION,
328                        null, null, null);
329                return setupOtherDirectories(mContext, directoryCursor, mAccount);
330            } finally {
331                if (directoryCursor != null) {
332                    directoryCursor.close();
333                }
334            }
335        } else {
336            // We don't need to search other directories.
337            return null;
338        }
339    }
340
341    /**
342     * An asynchronous filter that performs search in a particular directory.
343     */
344    protected class DirectoryFilter extends Filter {
345        private final DirectorySearchParams mParams;
346        private int mLimit;
347
348        public DirectoryFilter(DirectorySearchParams params) {
349            mParams = params;
350        }
351
352        public synchronized void setLimit(int limit) {
353            this.mLimit = limit;
354        }
355
356        public synchronized int getLimit() {
357            return this.mLimit;
358        }
359
360        @Override
361        protected FilterResults performFiltering(CharSequence constraint) {
362            if (DEBUG) {
363                Log.d(TAG, "DirectoryFilter#performFiltering. directoryId: " + mParams.directoryId
364                        + ", constraint: " + constraint + ", thread: " + Thread.currentThread());
365            }
366            final FilterResults results = new FilterResults();
367            results.values = null;
368            results.count = 0;
369
370            if (!TextUtils.isEmpty(constraint)) {
371                final ArrayList<TemporaryEntry> tempEntries = new ArrayList<TemporaryEntry>();
372
373                Cursor cursor = null;
374                try {
375                    // We don't want to pass this Cursor object to UI thread (b/5017608).
376                    // Assuming the result should contain fairly small results (at most ~10),
377                    // We just copy everything to local structure.
378                    cursor = doQuery(constraint, getLimit(), mParams.directoryId);
379
380                    if (cursor != null) {
381                        while (cursor.moveToNext()) {
382                            tempEntries.add(new TemporaryEntry(cursor, mParams.directoryId));
383                        }
384                    }
385                } finally {
386                    if (cursor != null) {
387                        cursor.close();
388                    }
389                }
390                if (!tempEntries.isEmpty()) {
391                    results.values = tempEntries;
392                    results.count = tempEntries.size();
393                }
394            }
395
396            if (DEBUG) {
397                Log.v(TAG, "finished loading directory \"" + mParams.displayName + "\"" +
398                        " with query " + constraint);
399            }
400
401            return results;
402        }
403
404        @Override
405        protected void publishResults(final CharSequence constraint, FilterResults results) {
406            if (DEBUG) {
407                Log.d(TAG, "DirectoryFilter#publishResult. constraint: " + constraint
408                        + ", mCurrentConstraint: " + mCurrentConstraint);
409            }
410            mDelayedMessageHandler.removeDelayedLoadMessage();
411            // Check if the received result matches the current constraint
412            // If not - the user must have continued typing after the request was issued, which
413            // means several member variables (like mRemainingDirectoryLoad) are already
414            // overwritten so shouldn't be touched here anymore.
415            if (TextUtils.equals(constraint, mCurrentConstraint)) {
416                if (results.count > 0) {
417                    @SuppressWarnings("unchecked")
418                    final ArrayList<TemporaryEntry> tempEntries =
419                            (ArrayList<TemporaryEntry>) results.values;
420
421                    for (TemporaryEntry tempEntry : tempEntries) {
422                        putOneEntry(tempEntry, mParams.directoryId == Directory.DEFAULT);
423                    }
424                }
425
426                // If there are remaining directories, set up delayed message again.
427                mRemainingDirectoryCount--;
428                if (mRemainingDirectoryCount > 0) {
429                    if (DEBUG) {
430                        Log.d(TAG, "Resend delayed load message. Current mRemainingDirectoryLoad: "
431                                + mRemainingDirectoryCount);
432                    }
433                    mDelayedMessageHandler.sendDelayedLoadMessage();
434                }
435
436                // If this directory result has some items, or there are no more directories that
437                // we are waiting for, clear the temp results
438                if (results.count > 0 || mRemainingDirectoryCount == 0) {
439                    // Clear the temp entries
440                    clearTempEntries();
441                }
442            }
443
444            // Show the list again without "waiting" message.
445            updateEntries(constructEntryList());
446        }
447    }
448
449    private final Context mContext;
450    private final ContentResolver mContentResolver;
451    private Account mAccount;
452    protected final int mPreferredMaxResultCount;
453    private DropdownChipLayouter mDropdownChipLayouter;
454
455    /**
456     * {@link #mEntries} is responsible for showing every result for this Adapter. To
457     * construct it, we use {@link #mEntryMap}, {@link #mNonAggregatedEntries}, and
458     * {@link #mExistingDestinations}.
459     *
460     * First, each destination (an email address or a phone number) with a valid contactId is
461     * inserted into {@link #mEntryMap} and grouped by the contactId. Destinations without valid
462     * contactId (possible if they aren't in local storage) are stored in
463     * {@link #mNonAggregatedEntries}.
464     * Duplicates are removed using {@link #mExistingDestinations}.
465     *
466     * After having all results from Cursor objects, all destinations in mEntryMap are copied to
467     * {@link #mEntries}. If the number of destinations is not enough (i.e. less than
468     * {@link #mPreferredMaxResultCount}), destinations in mNonAggregatedEntries are also used.
469     *
470     * These variables are only used in UI thread, thus should not be touched in
471     * performFiltering() methods.
472     */
473    private LinkedHashMap<Long, List<RecipientEntry>> mEntryMap;
474    private List<RecipientEntry> mNonAggregatedEntries;
475    private Set<String> mExistingDestinations;
476    /** Note: use {@link #updateEntries(List)} to update this variable. */
477    private List<RecipientEntry> mEntries;
478    private List<RecipientEntry> mTempEntries;
479
480    /** The number of directories this adapter is waiting for results. */
481    private int mRemainingDirectoryCount;
482
483    /**
484     * Used to ignore asynchronous queries with a different constraint, which may happen when
485     * users type characters quickly.
486     */
487    protected CharSequence mCurrentConstraint;
488
489    /**
490     * Performs all photo querying as well as caching for repeated lookups.
491     */
492    private PhotoManager mPhotoManager;
493
494    /**
495     * Handler specific for maintaining "Waiting for more contacts" message, which will be shown
496     * when:
497     * - there are directories to be searched
498     * - results from directories are slow to come
499     */
500    private final class DelayedMessageHandler extends Handler {
501        @Override
502        public void handleMessage(Message msg) {
503            if (mRemainingDirectoryCount > 0) {
504                updateEntries(constructEntryList());
505            }
506        }
507
508        public void sendDelayedLoadMessage() {
509            sendMessageDelayed(obtainMessage(MESSAGE_SEARCH_PENDING, 0, 0, null),
510                    MESSAGE_SEARCH_PENDING_DELAY);
511        }
512
513        public void removeDelayedLoadMessage() {
514            removeMessages(MESSAGE_SEARCH_PENDING);
515        }
516    }
517
518    private final DelayedMessageHandler mDelayedMessageHandler = new DelayedMessageHandler();
519
520    private EntriesUpdatedObserver mEntriesUpdatedObserver;
521
522    /**
523     * Constructor for email queries.
524     */
525    public BaseRecipientAdapter(Context context) {
526        this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, QUERY_TYPE_EMAIL);
527    }
528
529    public BaseRecipientAdapter(Context context, int preferredMaxResultCount) {
530        this(context, preferredMaxResultCount, QUERY_TYPE_EMAIL);
531    }
532
533    public BaseRecipientAdapter(int queryMode, Context context) {
534        this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, queryMode);
535    }
536
537    public BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount) {
538        this(context, preferredMaxResultCount, queryMode);
539    }
540
541    public BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode) {
542        mContext = context;
543        mContentResolver = context.getContentResolver();
544        mPreferredMaxResultCount = preferredMaxResultCount;
545        mPhotoManager = new DefaultPhotoManager(mContentResolver);
546        mQueryType = queryMode;
547
548        if (queryMode == QUERY_TYPE_EMAIL) {
549            mQueryMode = Queries.EMAIL;
550        } else if (queryMode == QUERY_TYPE_PHONE) {
551            mQueryMode = Queries.PHONE;
552        } else {
553            mQueryMode = Queries.EMAIL;
554            Log.e(TAG, "Unsupported query type: " + queryMode);
555        }
556    }
557
558    public Context getContext() {
559        return mContext;
560    }
561
562    public int getQueryType() {
563        return mQueryType;
564    }
565
566    public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
567        mDropdownChipLayouter = dropdownChipLayouter;
568        mDropdownChipLayouter.setQuery(mQueryMode);
569    }
570
571    public DropdownChipLayouter getDropdownChipLayouter() {
572        return mDropdownChipLayouter;
573    }
574
575    /**
576     * Enables overriding the default photo manager that is used.
577     */
578    public void setPhotoManager(PhotoManager photoManager) {
579        mPhotoManager = photoManager;
580    }
581
582    public PhotoManager getPhotoManager() {
583        return mPhotoManager;
584    }
585
586    /**
587     * If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter}
588     * instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when
589     * clicking on a chip. Default implementation returns {@code false}.
590     */
591    public boolean forceShowAddress() {
592        return false;
593    }
594
595    /**
596     * Used to replace email addresses with chips. Default behavior
597     * queries the ContactsProvider for contact information about the contact.
598     * Derived classes should override this method if they wish to use a
599     * new data source.
600     * @param inAddresses addresses to query
601     * @param callback callback to return results in case of success or failure
602     */
603    public void getMatchingRecipients(ArrayList<String> inAddresses,
604            RecipientAlternatesAdapter.RecipientMatchCallback callback) {
605        RecipientAlternatesAdapter.getMatchingRecipients(
606                getContext(), this, inAddresses, getAccount(), callback);
607    }
608
609    /**
610     * Set the account when known. Causes the search to prioritize contacts from that account.
611     */
612    @Override
613    public void setAccount(Account account) {
614        mAccount = account;
615    }
616
617    /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */
618    @Override
619    public Filter getFilter() {
620        return new DefaultFilter();
621    }
622
623    /**
624     * An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows
625     * additional sources of contacts to be considered as matching recipients.
626     * @param addresses A set of addresses to be matched
627     * @return A list of matches or null if none found
628     */
629    public Map<String, RecipientEntry> getMatchingRecipients(Set<String> addresses) {
630        return null;
631    }
632
633    public static List<DirectorySearchParams> setupOtherDirectories(Context context,
634            Cursor directoryCursor, Account account) {
635        final PackageManager packageManager = context.getPackageManager();
636        final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>();
637        DirectorySearchParams preferredDirectory = null;
638        while (directoryCursor.moveToNext()) {
639            final long id = directoryCursor.getLong(DirectoryListQuery.ID);
640
641            // Skip the local invisible directory, because the default directory already includes
642            // all local results.
643            if (id == Directory.LOCAL_INVISIBLE) {
644                continue;
645            }
646
647            final DirectorySearchParams params = new DirectorySearchParams();
648            final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME);
649            final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID);
650            params.directoryId = id;
651            params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME);
652            params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME);
653            params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE);
654            if (packageName != null && resourceId != 0) {
655                try {
656                    final Resources resources =
657                            packageManager.getResourcesForApplication(packageName);
658                    params.directoryType = resources.getString(resourceId);
659                    if (params.directoryType == null) {
660                        Log.e(TAG, "Cannot resolve directory name: "
661                                + resourceId + "@" + packageName);
662                    }
663                } catch (NameNotFoundException e) {
664                    Log.e(TAG, "Cannot resolve directory name: "
665                            + resourceId + "@" + packageName, e);
666                }
667            }
668
669            // If an account has been provided and we found a directory that
670            // corresponds to that account, place that directory second, directly
671            // underneath the local contacts.
672            if (account != null && account.name.equals(params.accountName) &&
673                    account.type.equals(params.accountType)) {
674                preferredDirectory = params;
675            } else {
676                paramsList.add(params);
677            }
678        }
679
680        if (preferredDirectory != null) {
681            paramsList.add(1, preferredDirectory);
682        }
683
684        return paramsList;
685    }
686
687    /**
688     * Starts search in other directories using {@link Filter}. Results will be handled in
689     * {@link DirectoryFilter}.
690     */
691    protected void startSearchOtherDirectories(
692            CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) {
693        final int count = paramsList.size();
694        // Note: skipping the default partition (index 0), which has already been loaded
695        for (int i = 1; i < count; i++) {
696            final DirectorySearchParams params = paramsList.get(i);
697            params.constraint = constraint;
698            if (params.filter == null) {
699                params.filter = new DirectoryFilter(params);
700            }
701            params.filter.setLimit(limit);
702            params.filter.filter(constraint);
703        }
704
705        // Directory search started. We may show "waiting" message if directory results are slow
706        // enough.
707        mRemainingDirectoryCount = count - 1;
708        mDelayedMessageHandler.sendDelayedLoadMessage();
709    }
710
711    /**
712     * Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter}
713     * wants to add an additional entry to the results. Derived classes should override
714     * this method if they are not using the default data structures provided by
715     * {@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their
716     * own data structures to store and collate data.
717     * @param entry the entry being added
718     * @param isAggregatedEntry
719     */
720    protected void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry) {
721        putOneEntry(entry, isAggregatedEntry,
722                mEntryMap, mNonAggregatedEntries, mExistingDestinations);
723    }
724
725    private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry,
726            LinkedHashMap<Long, List<RecipientEntry>> entryMap,
727            List<RecipientEntry> nonAggregatedEntries,
728            Set<String> existingDestinations) {
729        if (existingDestinations.contains(entry.destination)) {
730            return;
731        }
732
733        existingDestinations.add(entry.destination);
734
735        if (!isAggregatedEntry) {
736            nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry(
737                    entry.displayName,
738                    entry.displayNameSource,
739                    entry.destination, entry.destinationType, entry.destinationLabel,
740                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
741                    true, entry.lookupKey));
742        } else if (entryMap.containsKey(entry.contactId)) {
743            // We already have a section for the person.
744            final List<RecipientEntry> entryList = entryMap.get(entry.contactId);
745            entryList.add(RecipientEntry.constructSecondLevelEntry(
746                    entry.displayName,
747                    entry.displayNameSource,
748                    entry.destination, entry.destinationType, entry.destinationLabel,
749                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
750                    true, entry.lookupKey));
751        } else {
752            final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>();
753            entryList.add(RecipientEntry.constructTopLevelEntry(
754                    entry.displayName,
755                    entry.displayNameSource,
756                    entry.destination, entry.destinationType, entry.destinationLabel,
757                    entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
758                    true, entry.lookupKey));
759            entryMap.put(entry.contactId, entryList);
760        }
761    }
762
763    /**
764     * Returns the actual list to use for this Adapter. Derived classes
765     * should override this method if overriding how the adapter stores and collates
766     * data.
767     */
768    protected List<RecipientEntry> constructEntryList() {
769        return constructEntryList(mEntryMap, mNonAggregatedEntries);
770    }
771
772    /**
773     * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to
774     * fetch a cached photo for each contact entry (other than separators), or request another
775     * thread to get one from directories.
776     */
777    private List<RecipientEntry> constructEntryList(
778            LinkedHashMap<Long, List<RecipientEntry>> entryMap,
779            List<RecipientEntry> nonAggregatedEntries) {
780        final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
781        int validEntryCount = 0;
782        for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) {
783            final List<RecipientEntry> entryList = mapEntry.getValue();
784            final int size = entryList.size();
785            for (int i = 0; i < size; i++) {
786                RecipientEntry entry = entryList.get(i);
787                entries.add(entry);
788                mPhotoManager.populatePhotoBytesAsync(entry, this);
789                validEntryCount++;
790            }
791            if (validEntryCount > mPreferredMaxResultCount) {
792                break;
793            }
794        }
795        if (validEntryCount <= mPreferredMaxResultCount) {
796            for (RecipientEntry entry : nonAggregatedEntries) {
797                if (validEntryCount > mPreferredMaxResultCount) {
798                    break;
799                }
800                entries.add(entry);
801                mPhotoManager.populatePhotoBytesAsync(entry, this);
802                validEntryCount++;
803            }
804        }
805
806        return entries;
807    }
808
809
810    public interface EntriesUpdatedObserver {
811        public void onChanged(List<RecipientEntry> entries);
812    }
813
814    public void registerUpdateObserver(EntriesUpdatedObserver observer) {
815        mEntriesUpdatedObserver = observer;
816    }
817
818    /** Resets {@link #mEntries} and notify the event to its parent ListView. */
819    protected void updateEntries(List<RecipientEntry> newEntries) {
820        mEntries = newEntries;
821        mEntriesUpdatedObserver.onChanged(newEntries);
822        notifyDataSetChanged();
823    }
824
825    /**
826     * If there are no local results and we are searching alternate results,
827     * in the new result set, cache off what had been shown to the user for use until
828     * the first directory result is returned
829     * @param newEntryCount number of newly loaded entries
830     * @param paramListCount number of alternate filters it will search (including the current one).
831     */
832    protected void cacheCurrentEntriesIfNeeded(int newEntryCount, int paramListCount) {
833        if (newEntryCount == 0 && paramListCount > 1) {
834            cacheCurrentEntries();
835        }
836    }
837
838    protected void cacheCurrentEntries() {
839        mTempEntries = mEntries;
840    }
841
842    protected void clearTempEntries() {
843        mTempEntries = null;
844    }
845
846    protected List<RecipientEntry> getEntries() {
847        return mTempEntries != null ? mTempEntries : mEntries;
848    }
849
850    protected void fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb) {
851        mPhotoManager.populatePhotoBytesAsync(entry, cb);
852    }
853
854    private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) {
855        final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon()
856                .appendPath(constraint.toString())
857                .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
858                        String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES));
859        if (directoryId != null) {
860            builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
861                    String.valueOf(directoryId));
862        }
863        if (mAccount != null) {
864            builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name);
865            builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type);
866        }
867        final long start = System.currentTimeMillis();
868        final Cursor cursor = mContentResolver.query(
869                builder.build(), mQueryMode.getProjection(), null, null, null);
870        final long end = System.currentTimeMillis();
871        if (DEBUG) {
872            Log.d(TAG, "Time for autocomplete (query: " + constraint
873                    + ", directoryId: " + directoryId + ", num_of_results: "
874                    + (cursor != null ? cursor.getCount() : "null") + "): "
875                    + (end - start) + " ms");
876        }
877        return cursor;
878    }
879
880    // TODO: This won't be used at all. We should find better way to quit the thread..
881    /*public void close() {
882        mEntries = null;
883        mPhotoCacheMap.evictAll();
884        if (!sPhotoHandlerThread.quit()) {
885            Log.w(TAG, "Failed to quit photo handler thread, ignoring it.");
886        }
887    }*/
888
889    @Override
890    public int getCount() {
891        final List<RecipientEntry> entries = getEntries();
892        return entries != null ? entries.size() : 0;
893    }
894
895    @Override
896    public RecipientEntry getItem(int position) {
897        return getEntries().get(position);
898    }
899
900    @Override
901    public long getItemId(int position) {
902        return position;
903    }
904
905    @Override
906    public int getViewTypeCount() {
907        return RecipientEntry.ENTRY_TYPE_SIZE;
908    }
909
910    @Override
911    public int getItemViewType(int position) {
912        return getEntries().get(position).getEntryType();
913    }
914
915    @Override
916    public boolean isEnabled(int position) {
917        return getEntries().get(position).isSelectable();
918    }
919
920    @Override
921    public View getView(int position, View convertView, ViewGroup parent) {
922        final RecipientEntry entry = getEntries().get(position);
923
924        final String constraint = mCurrentConstraint == null ? null :
925                mCurrentConstraint.toString();
926
927        return mDropdownChipLayouter.bindView(convertView, parent, entry, position,
928                AdapterType.BASE_RECIPIENT, constraint);
929    }
930
931    public Account getAccount() {
932        return mAccount;
933    }
934
935    @Override
936    public void onPhotoBytesPopulated() {
937        // Default implementation does nothing
938    }
939
940    @Override
941    public void onPhotoBytesAsynchronouslyPopulated() {
942        notifyDataSetChanged();
943    }
944
945    @Override
946    public void onPhotoBytesAsyncLoadFailed() {
947        // Default implementation does nothing
948    }
949}
950