Home | History | Annotate | Download | only in chips
      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 
     17 package com.android.ex.chips;
     18 
     19 import android.accounts.Account;
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.content.pm.PackageManager;
     23 import android.content.pm.PackageManager.NameNotFoundException;
     24 import android.content.res.Resources;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.os.Handler;
     28 import android.os.Message;
     29 import android.provider.ContactsContract;
     30 import android.provider.ContactsContract.Directory;
     31 import android.text.TextUtils;
     32 import android.text.util.Rfc822Token;
     33 import android.util.Log;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 import android.widget.AutoCompleteTextView;
     37 import android.widget.BaseAdapter;
     38 import android.widget.Filter;
     39 import android.widget.Filterable;
     40 
     41 import com.android.ex.chips.DropdownChipLayouter.AdapterType;
     42 
     43 import java.util.ArrayList;
     44 import java.util.Collections;
     45 import java.util.HashSet;
     46 import java.util.LinkedHashMap;
     47 import java.util.List;
     48 import java.util.Map;
     49 import java.util.Set;
     50 
     51 /**
     52  * Adapter for showing a recipient list.
     53  */
     54 public 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