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