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