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