am 5179b9a3: (-s ours) Import translations. DO NOT MERGE

* commit '5179b9a3f9b4daec5ed66594b0ef75cf93d0f1d3':
  Import translations. DO NOT MERGE
diff --git a/sample/Android.mk b/sample/Android.mk
index e0dd9ca..d58e4b5 100644
--- a/sample/Android.mk
+++ b/sample/Android.mk
@@ -16,7 +16,7 @@
 
 # Include res dir from chips
 chips_dir := ../res
-res_dirs := res $(chips_dir)
+local_res_dirs := res $(chips_dir)
 
 ##################################################
 # Build APK
@@ -31,7 +31,7 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src) \
         $(call all-logtags-files-under, $(src_dirs))
-LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
+LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(local_res_dirs))
 LOCAL_AAPT_FLAGS := --auto-add-overlay
 LOCAL_AAPT_FLAGS += --extra-packages com.android.ex.chips
 
diff --git a/sample/src/com/android/ex/chips/sample/MainActivity.java b/sample/src/com/android/ex/chips/sample/MainActivity.java
index 0622e65..f4be673 100644
--- a/sample/src/com/android/ex/chips/sample/MainActivity.java
+++ b/sample/src/com/android/ex/chips/sample/MainActivity.java
@@ -33,13 +33,13 @@
         final RecipientEditTextView emailRetv =
                 (RecipientEditTextView) findViewById(R.id.email_retv);
         emailRetv.setTokenizer(new Rfc822Tokenizer());
-        emailRetv.setAdapter(new BaseRecipientAdapter(this) { });
+        emailRetv.setAdapter(new BaseRecipientAdapter(this));
 
         final RecipientEditTextView phoneRetv =
                 (RecipientEditTextView) findViewById(R.id.phone_retv);
         phoneRetv.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
         phoneRetv.setAdapter(
-                new BaseRecipientAdapter(BaseRecipientAdapter.QUERY_TYPE_PHONE, this) { });
+                new BaseRecipientAdapter(BaseRecipientAdapter.QUERY_TYPE_PHONE, this));
     }
 
 }
diff --git a/src/com/android/ex/chips/BaseRecipientAdapter.java b/src/com/android/ex/chips/BaseRecipientAdapter.java
index 468e168..24f7a3e 100644
--- a/src/com/android/ex/chips/BaseRecipientAdapter.java
+++ b/src/com/android/ex/chips/BaseRecipientAdapter.java
@@ -23,20 +23,14 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.Message;
 import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.Directory;
-import android.support.v4.util.LruCache;
 import android.text.TextUtils;
 import android.text.util.Rfc822Token;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AutoCompleteTextView;
@@ -46,11 +40,8 @@
 
 import com.android.ex.chips.DropdownChipLayouter.AdapterType;
 
-import java.io.ByteArrayOutputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -60,7 +51,8 @@
 /**
  * Adapter for showing a recipient list.
  */
-public class BaseRecipientAdapter extends BaseAdapter implements Filterable, AccountSpecifier {
+public class BaseRecipientAdapter extends BaseAdapter implements Filterable, AccountSpecifier,
+        PhotoManager.PhotoManagerCallback {
     private static final String TAG = "BaseRecipientAdapter";
 
     private static final boolean DEBUG = false;
@@ -83,9 +75,6 @@
     // This is ContactsContract.PRIMARY_ACCOUNT_TYPE. Available from ICS as hidden
     static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account";
 
-    /** The number of photos cached in this Adapter. */
-    private static final int PHOTO_CACHE_SIZE = 20;
-
     /**
      * The "Waiting for more contacts" message will be displayed if search is not complete
      * within this many milliseconds.
@@ -97,7 +86,7 @@
     public static final int QUERY_TYPE_EMAIL = 0;
     public static final int QUERY_TYPE_PHONE = 1;
 
-    private final Queries.Query mQuery;
+    private final Queries.Query mQueryMode;
     private final int mQueryType;
 
     /**
@@ -113,14 +102,6 @@
         public DirectoryFilter filter;
     }
 
-    private static class PhotoQuery {
-        public static final String[] PROJECTION = {
-            Photo.PHOTO
-        };
-
-        public static final int PHOTO = 0;
-    }
-
     protected static class DirectoryListQuery {
 
         public static final Uri URI =
@@ -269,24 +250,8 @@
                     final List<RecipientEntry> entries = constructEntryList(
                             entryMap, nonAggregatedEntries);
 
-                    // After having local results, check the size of results. If the results are
-                    // not enough, we search remote directories, which will take longer time.
-                    final int limit = mPreferredMaxResultCount - existingDestinations.size();
-                    final List<DirectorySearchParams> paramsList;
-                    if (limit > 0) {
-                        if (DEBUG) {
-                            Log.d(TAG, "More entries should be needed (current: "
-                                    + existingDestinations.size()
-                                    + ", remaining limit: " + limit + ") ");
-                        }
-                        directoryCursor = mContentResolver.query(
-                                DirectoryListQuery.URI, DirectoryListQuery.PROJECTION,
-                                null, null, null);
-                        paramsList = setupOtherDirectories(mContext, directoryCursor, mAccount);
-                    } else {
-                        // We don't need to search other directories.
-                        paramsList = null;
-                    }
+                    final List<DirectorySearchParams> paramsList =
+                            searchOtherDirectories(existingDestinations);
 
                     results.values = new DefaultFilterResult(
                             entries, entryMap, nonAggregatedEntries,
@@ -306,9 +271,6 @@
 
         @Override
         protected void publishResults(final CharSequence constraint, FilterResults results) {
-            // If a user types a string very quickly and database is slow, "constraint" refers to
-            // an older text which shows inconsistent results for users obsolete (b/4998713).
-            // TODO: Fix it.
             mCurrentConstraint = constraint;
 
             clearTempEntries();
@@ -334,8 +296,9 @@
                             defaultFilterResult.existingDestinations.size();
                     startSearchOtherDirectories(constraint, defaultFilterResult.paramsList, limit);
                 }
+            } else {
+                updateEntries(Collections.<RecipientEntry>emptyList());
             }
-
         }
 
         @Override
@@ -351,6 +314,26 @@
         }
     }
 
+    protected List<DirectorySearchParams> searchOtherDirectories(Set<String> existingDestinations) {
+        // After having local results, check the size of results. If the results are
+        // not enough, we search remote directories, which will take longer time.
+        final int limit = mPreferredMaxResultCount - existingDestinations.size();
+        if (limit > 0) {
+            if (DEBUG) {
+                Log.d(TAG, "More entries should be needed (current: "
+                        + existingDestinations.size()
+                        + ", remaining limit: " + limit + ") ");
+            }
+            final Cursor directoryCursor = mContentResolver.query(
+                    DirectoryListQuery.URI, DirectoryListQuery.PROJECTION,
+                    null, null, null);
+            return setupOtherDirectories(mContext, directoryCursor, mAccount);
+        } else {
+            // We don't need to search other directories.
+            return null;
+        }
+    }
+
     /**
      * An asynchronous filter that performs search in a particular directory.
      */
@@ -432,8 +415,7 @@
                             (ArrayList<TemporaryEntry>) results.values;
 
                     for (TemporaryEntry tempEntry : tempEntries) {
-                        putOneEntry(tempEntry, mParams.directoryId == Directory.DEFAULT,
-                                mEntryMap, mNonAggregatedEntries, mExistingDestinations);
+                        putOneEntry(tempEntry, mParams.directoryId == Directory.DEFAULT);
                     }
                 }
 
@@ -456,15 +438,14 @@
             }
 
             // Show the list again without "waiting" message.
-            updateEntries(constructEntryList(mEntryMap, mNonAggregatedEntries));
+            updateEntries(constructEntryList());
         }
     }
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
-    private final LayoutInflater mInflater;
     private Account mAccount;
-    private final int mPreferredMaxResultCount;
+    protected final int mPreferredMaxResultCount;
     private DropdownChipLayouter mDropdownChipLayouter;
 
     /**
@@ -499,9 +480,12 @@
      * Used to ignore asynchronous queries with a different constraint, which may happen when
      * users type characters quickly.
      */
-    private CharSequence mCurrentConstraint;
+    protected CharSequence mCurrentConstraint;
 
-    private final LruCache<Uri, byte[]> mPhotoCacheMap;
+    /**
+     * Performs all photo querying as well as caching for repeated lookups.
+     */
+    private PhotoManager mPhotoManager;
 
     /**
      * Handler specific for maintaining "Waiting for more contacts" message, which will be shown
@@ -513,7 +497,7 @@
         @Override
         public void handleMessage(Message msg) {
             if (mRemainingDirectoryCount > 0) {
-                updateEntries(constructEntryList(mEntryMap, mNonAggregatedEntries));
+                updateEntries(constructEntryList());
             }
         }
 
@@ -553,17 +537,16 @@
     public BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode) {
         mContext = context;
         mContentResolver = context.getContentResolver();
-        mInflater = LayoutInflater.from(context);
         mPreferredMaxResultCount = preferredMaxResultCount;
-        mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE);
+        mPhotoManager = new DefaultPhotoManager(mContentResolver);
         mQueryType = queryMode;
 
         if (queryMode == QUERY_TYPE_EMAIL) {
-            mQuery = Queries.EMAIL;
+            mQueryMode = Queries.EMAIL;
         } else if (queryMode == QUERY_TYPE_PHONE) {
-            mQuery = Queries.PHONE;
+            mQueryMode = Queries.PHONE;
         } else {
-            mQuery = Queries.EMAIL;
+            mQueryMode = Queries.EMAIL;
             Log.e(TAG, "Unsupported query type: " + queryMode);
         }
     }
@@ -578,7 +561,7 @@
 
     public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
         mDropdownChipLayouter = dropdownChipLayouter;
-        mDropdownChipLayouter.setQuery(mQuery);
+        mDropdownChipLayouter.setQuery(mQueryMode);
     }
 
     public DropdownChipLayouter getDropdownChipLayouter() {
@@ -586,6 +569,40 @@
     }
 
     /**
+     * Enables overriding the default photo manager that is used.
+     */
+    public void setPhotoManager(PhotoManager photoManager) {
+        mPhotoManager = photoManager;
+    }
+
+    public PhotoManager getPhotoManager() {
+        return mPhotoManager;
+    }
+
+    /**
+     * If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter}
+     * instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when
+     * clicking on a chip. Default implementation returns {@code false}.
+     */
+    public boolean forceShowAddress() {
+        return false;
+    }
+
+    /**
+     * Used to replace email addresses with chips. Default behavior
+     * queries the ContactsProvider for contact information about the contact.
+     * Derived classes should override this method if they wish to use a
+     * new data source.
+     * @param inAddresses addresses to query
+     * @param callback callback to return results in case of success or failure
+     */
+    public void getMatchingRecipients(ArrayList<String> inAddresses,
+            RecipientAlternatesAdapter.RecipientMatchCallback callback) {
+        RecipientAlternatesAdapter.getMatchingRecipients(
+                getContext(), this, inAddresses, getAccount(), callback);
+    }
+
+    /**
      * Set the account when known. Causes the search to prioritize contacts from that account.
      */
     @Override
@@ -600,7 +617,7 @@
     }
 
     /**
-     * An extesion to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows
+     * An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows
      * additional sources of contacts to be considered as matching recipients.
      * @param addresses A set of addresses to be matched
      * @return A list of matches or null if none found
@@ -687,6 +704,20 @@
         mDelayedMessageHandler.sendDelayedLoadMessage();
     }
 
+    /**
+     * Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter}
+     * wants to add an additional entry to the results. Derived classes should override
+     * this method if they are not using the default data structures provided by
+     * {@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their
+     * own data structures to store and collate data.
+     * @param entry the entry being added
+     * @param isAggregatedEntry
+     */
+    protected void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry) {
+        putOneEntry(entry, isAggregatedEntry,
+                mEntryMap, mNonAggregatedEntries, mExistingDestinations);
+    }
+
     private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry,
             LinkedHashMap<Long, List<RecipientEntry>> entryMap,
             List<RecipientEntry> nonAggregatedEntries,
@@ -726,6 +757,15 @@
     }
 
     /**
+     * Returns the actual list to use for this Adapter. Derived classes
+     * should override this method if overriding how the adapter stores and collates
+     * data.
+     */
+    protected List<RecipientEntry> constructEntryList() {
+        return constructEntryList(mEntryMap, mNonAggregatedEntries);
+    }
+
+    /**
      * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to
      * fetch a cached photo for each contact entry (other than separators), or request another
      * thread to get one from directories.
@@ -741,7 +781,7 @@
             for (int i = 0; i < size; i++) {
                 RecipientEntry entry = entryList.get(i);
                 entries.add(entry);
-                tryFetchPhoto(entry);
+                mPhotoManager.populatePhotoBytesAsync(entry, this);
                 validEntryCount++;
             }
             if (validEntryCount > mPreferredMaxResultCount) {
@@ -754,8 +794,7 @@
                     break;
                 }
                 entries.add(entry);
-                tryFetchPhoto(entry);
-
+                mPhotoManager.populatePhotoBytesAsync(entry, this);
                 validEntryCount++;
             }
         }
@@ -773,17 +812,17 @@
     }
 
     /** Resets {@link #mEntries} and notify the event to its parent ListView. */
-    private void updateEntries(List<RecipientEntry> newEntries) {
+    protected void updateEntries(List<RecipientEntry> newEntries) {
         mEntries = newEntries;
         mEntriesUpdatedObserver.onChanged(newEntries);
         notifyDataSetChanged();
     }
 
-    private void cacheCurrentEntries() {
+    protected void cacheCurrentEntries() {
         mTempEntries = mEntries;
     }
 
-    private void clearTempEntries() {
+    protected void clearTempEntries() {
         mTempEntries = null;
     }
 
@@ -791,139 +830,12 @@
         return mTempEntries != null ? mTempEntries : mEntries;
     }
 
-    private void tryFetchPhoto(final RecipientEntry entry) {
-        final Uri photoThumbnailUri = entry.getPhotoThumbnailUri();
-        if (photoThumbnailUri != null) {
-            final byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri);
-            if (photoBytes != null) {
-                entry.setPhotoBytes(photoBytes);
-                // notifyDataSetChanged() should be called by a caller.
-            } else {
-                if (DEBUG) {
-                    Log.d(TAG, "No photo cache for " + entry.getDisplayName()
-                            + ". Fetch one asynchronously");
-                }
-                fetchPhotoAsync(entry, photoThumbnailUri);
-            }
-        }
-    }
-
-    // For reading photos for directory contacts, this is the chunksize for
-    // copying from the inputstream to the output stream.
-    private static final int BUFFER_SIZE = 1024*16;
-
-    private void fetchPhotoAsync(final RecipientEntry entry, final Uri photoThumbnailUri) {
-        final AsyncTask<Void, Void, byte[]> photoLoadTask = new AsyncTask<Void, Void, byte[]>() {
-            @Override
-            protected byte[] doInBackground(Void... params) {
-                // First try running a query. Images for local contacts are
-                // loaded by sending a query to the ContactsProvider.
-                final Cursor photoCursor = mContentResolver.query(
-                        photoThumbnailUri, PhotoQuery.PROJECTION, null, null, null);
-                if (photoCursor != null) {
-                    try {
-                        if (photoCursor.moveToFirst()) {
-                            return photoCursor.getBlob(PhotoQuery.PHOTO);
-                        }
-                    } finally {
-                        photoCursor.close();
-                    }
-                } else {
-                    // If the query fails, try streaming the URI directly.
-                    // For remote directory images, this URI resolves to the
-                    // directory provider and the images are loaded by sending
-                    // an openFile call to the provider.
-                    try {
-                        InputStream is = mContentResolver.openInputStream(
-                                photoThumbnailUri);
-                        if (is != null) {
-                            byte[] buffer = new byte[BUFFER_SIZE];
-                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                            try {
-                                int size;
-                                while ((size = is.read(buffer)) != -1) {
-                                    baos.write(buffer, 0, size);
-                                }
-                            } finally {
-                                is.close();
-                            }
-                            return baos.toByteArray();
-                        }
-                    } catch (IOException ex) {
-                        // ignore
-                    }
-                }
-                return null;
-            }
-
-            @Override
-            protected void onPostExecute(final byte[] photoBytes) {
-                entry.setPhotoBytes(photoBytes);
-                if (photoBytes != null) {
-                    mPhotoCacheMap.put(photoThumbnailUri, photoBytes);
-                    notifyDataSetChanged();
-                }
-            }
-        };
-        photoLoadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
-    }
-
-    protected void fetchPhoto(final RecipientEntry entry, final Uri photoThumbnailUri) {
-        byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri);
-        if (photoBytes != null) {
-            entry.setPhotoBytes(photoBytes);
-            return;
-        }
-        final Cursor photoCursor = mContentResolver.query(photoThumbnailUri, PhotoQuery.PROJECTION,
-                null, null, null);
-        if (photoCursor != null) {
-            try {
-                if (photoCursor.moveToFirst()) {
-                    photoBytes = photoCursor.getBlob(PhotoQuery.PHOTO);
-                    entry.setPhotoBytes(photoBytes);
-                    mPhotoCacheMap.put(photoThumbnailUri, photoBytes);
-                }
-            } finally {
-                photoCursor.close();
-            }
-        } else {
-            InputStream inputStream = null;
-            ByteArrayOutputStream outputStream = null;
-            try {
-                inputStream = mContentResolver.openInputStream(photoThumbnailUri);
-                final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
-
-                if (bitmap != null) {
-                    outputStream = new ByteArrayOutputStream();
-                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
-                    photoBytes = outputStream.toByteArray();
-
-                    entry.setPhotoBytes(photoBytes);
-                    mPhotoCacheMap.put(photoThumbnailUri, photoBytes);
-                }
-            } catch (final FileNotFoundException e) {
-                Log.w(TAG, "Error opening InputStream for photo", e);
-            } finally {
-                try {
-                    if (inputStream != null) {
-                        inputStream.close();
-                    }
-                } catch (IOException e) {
-                    Log.e(TAG, "Error closing photo input stream", e);
-                }
-                try {
-                    if (outputStream != null) {
-                        outputStream.close();
-                    }
-                } catch (IOException e) {
-                    Log.e(TAG, "Error closing photo output stream", e);
-                }
-            }
-        }
+    protected void fetchPhoto(final RecipientEntry entry, PhotoManager.PhotoManagerCallback cb) {
+        mPhotoManager.populatePhotoBytesAsync(entry, cb);
     }
 
     private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) {
-        final Uri.Builder builder = mQuery.getContentFilterUri().buildUpon()
+        final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon()
                 .appendPath(constraint.toString())
                 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
                         String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES));
@@ -937,7 +849,7 @@
         }
         final long start = System.currentTimeMillis();
         final Cursor cursor = mContentResolver.query(
-                builder.build(), mQuery.getProjection(), null, null, null);
+                builder.build(), mQueryMode.getProjection(), null, null, null);
         final long end = System.currentTimeMillis();
         if (DEBUG) {
             Log.d(TAG, "Time for autocomplete (query: " + constraint
@@ -1002,4 +914,19 @@
     public Account getAccount() {
         return mAccount;
     }
+
+    @Override
+    public void onPhotoBytesPopulated() {
+        // Default implementation does nothing
+    }
+
+    @Override
+    public void onPhotoBytesAsynchronouslyPopulated() {
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public void onPhotoBytesAsyncLoadFailed() {
+        // Default implementation does nothing
+    }
 }
diff --git a/src/com/android/ex/chips/DefaultPhotoManager.java b/src/com/android/ex/chips/DefaultPhotoManager.java
new file mode 100644
index 0000000..b9c001d
--- /dev/null
+++ b/src/com/android/ex/chips/DefaultPhotoManager.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ex.chips;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract;
+import android.support.v4.util.LruCache;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Default implementation of {@link com.android.ex.chips.PhotoManager} that
+ * queries for photo bytes by using the {@link com.android.ex.chips.RecipientEntry}'s
+ * photoThumbnailUri.
+ */
+public class DefaultPhotoManager implements PhotoManager {
+    private static final String TAG = "DefaultPhotoManager";
+
+    private static final boolean DEBUG = false;
+
+    /**
+     * For reading photos for directory contacts, this is the chunk size for
+     * copying from the {@link InputStream} to the output stream.
+     */
+    private static final int BUFFER_SIZE = 1024*16;
+
+    private static class PhotoQuery {
+        public static final String[] PROJECTION = {
+            ContactsContract.CommonDataKinds.Photo.PHOTO
+        };
+
+        public static final int PHOTO = 0;
+    }
+
+    private final ContentResolver mContentResolver;
+    private final LruCache<Uri, byte[]> mPhotoCacheMap;
+
+    public DefaultPhotoManager(ContentResolver contentResolver) {
+        mContentResolver = contentResolver;
+        mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE);
+    }
+
+    @Override
+    public void populatePhotoBytesAsync(RecipientEntry entry, PhotoManagerCallback callback) {
+        final Uri photoThumbnailUri = entry.getPhotoThumbnailUri();
+        if (photoThumbnailUri != null) {
+            final byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri);
+            if (photoBytes != null) {
+                entry.setPhotoBytes(photoBytes);
+                if (callback != null) {
+                    callback.onPhotoBytesPopulated();
+                }
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "No photo cache for " + entry.getDisplayName()
+                            + ". Fetch one asynchronously");
+                }
+                fetchPhotoAsync(entry, photoThumbnailUri, callback);
+            }
+        } else if (callback != null) {
+            callback.onPhotoBytesAsyncLoadFailed();
+        }
+    }
+
+    private void fetchPhotoAsync(final RecipientEntry entry, final Uri photoThumbnailUri,
+            final PhotoManagerCallback callback) {
+        final AsyncTask<Void, Void, byte[]> photoLoadTask = new AsyncTask<Void, Void, byte[]>() {
+            @Override
+            protected byte[] doInBackground(Void... params) {
+                // First try running a query. Images for local contacts are
+                // loaded by sending a query to the ContactsProvider.
+                final Cursor photoCursor = mContentResolver.query(
+                        photoThumbnailUri, PhotoQuery.PROJECTION, null, null, null);
+                if (photoCursor != null) {
+                    try {
+                        if (photoCursor.moveToFirst()) {
+                            return photoCursor.getBlob(PhotoQuery.PHOTO);
+                        }
+                    } finally {
+                        photoCursor.close();
+                    }
+                } else {
+                    // If the query fails, try streaming the URI directly.
+                    // For remote directory images, this URI resolves to the
+                    // directory provider and the images are loaded by sending
+                    // an openFile call to the provider.
+                    try {
+                        InputStream is = mContentResolver.openInputStream(
+                                photoThumbnailUri);
+                        if (is != null) {
+                            byte[] buffer = new byte[BUFFER_SIZE];
+                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                            try {
+                                int size;
+                                while ((size = is.read(buffer)) != -1) {
+                                    baos.write(buffer, 0, size);
+                                }
+                            } finally {
+                                is.close();
+                            }
+                            return baos.toByteArray();
+                        }
+                    } catch (IOException ex) {
+                        // ignore
+                    }
+                }
+                return null;
+            }
+
+            @Override
+            protected void onPostExecute(final byte[] photoBytes) {
+                entry.setPhotoBytes(photoBytes);
+                if (photoBytes != null) {
+                    mPhotoCacheMap.put(photoThumbnailUri, photoBytes);
+                    if (callback != null) {
+                        callback.onPhotoBytesAsynchronouslyPopulated();
+                    }
+                } else if (callback != null) {
+                    callback.onPhotoBytesAsyncLoadFailed();
+                }
+            }
+        };
+        photoLoadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+    }
+}
diff --git a/src/com/android/ex/chips/PhotoManager.java b/src/com/android/ex/chips/PhotoManager.java
new file mode 100644
index 0000000..66a64c2
--- /dev/null
+++ b/src/com/android/ex/chips/PhotoManager.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ex.chips;
+
+/**
+ * Used by the {@link com.android.ex.chips.BaseRecipientAdapter} to handle fetching
+ * photos from external sources and caching them for faster lookup later.
+ */
+public interface PhotoManager {
+
+    /** The number of photos cached in this Adapter. */
+    public static final int PHOTO_CACHE_SIZE = 20;
+
+    /**
+     * Sets the {@link com.android.ex.chips.RecipientEntry}'s photo bytes. If the photo bytes
+     * are cached, this action happens immediately. Otherwise, the work to fetch the photo
+     * bytes is performed asynchronously before setting the value on the UI thread.<p/>
+     *
+     * If the photo bytes were fetched asynchronously,
+     * {@link PhotoManagerCallback#onPhotoBytesAsynchronouslyPopulated()} is called. This
+     * method is not called if the photo bytes have been cached previously (because no
+     * asynchronous work was performed). In that case,
+     * {@link PhotoManagerCallback#onPhotoBytesPopulated()} is called.
+     */
+    void populatePhotoBytesAsync(RecipientEntry entry, PhotoManagerCallback callback);
+
+    interface PhotoManagerCallback {
+        void onPhotoBytesPopulated();
+        void onPhotoBytesAsynchronouslyPopulated();
+        void onPhotoBytesAsyncLoadFailed();
+    }
+}
diff --git a/src/com/android/ex/chips/RecipientAlternatesAdapter.java b/src/com/android/ex/chips/RecipientAlternatesAdapter.java
index f6f662d..d091fa6 100644
--- a/src/com/android/ex/chips/RecipientAlternatesAdapter.java
+++ b/src/com/android/ex/chips/RecipientAlternatesAdapter.java
@@ -49,7 +49,7 @@
  * queried by email or by phone number.
  */
 public class RecipientAlternatesAdapter extends CursorAdapter {
-    static final int MAX_LOOKUPS = 50;
+    public static final int MAX_LOOKUPS = 50;
 
     private final long mCurrentId;
 
@@ -87,7 +87,6 @@
      * @param context Context.
      * @param inAddresses Array of addresses on which to perform the lookup.
      * @param callback RecipientMatchCallback called when a match or matches are found.
-     * @return HashMap<String,RecipientEntry>
      */
     public static void getMatchingRecipients(Context context, BaseRecipientAdapter adapter,
             ArrayList<String> inAddresses, int addressType, Account account,
@@ -133,9 +132,31 @@
                 c.close();
             }
         }
+
+        final Set<String> matchesNotFound = new HashSet<String>();
+
+        getMatchingRecipientsFromDirectoryQueries(context, recipientEntries,
+                addresses, account, matchesNotFound, query, callback);
+
+        getMatchingRecipientsFromExtensionMatcher(adapter, matchesNotFound, callback);
+    }
+
+    public static void getMatchingRecipientsFromDirectoryQueries(Context context,
+            Map<String, RecipientEntry> recipientEntries, Set<String> addresses,
+            Account account, Set<String> matchesNotFound,
+            RecipientMatchCallback callback) {
+        getMatchingRecipientsFromDirectoryQueries(
+                context, recipientEntries, addresses, account,
+                matchesNotFound, Queries.EMAIL, callback);
+    }
+
+    private static void getMatchingRecipientsFromDirectoryQueries(Context context,
+            Map<String, RecipientEntry> recipientEntries, Set<String> addresses,
+            Account account, Set<String> matchesNotFound, Queries.Query query,
+            RecipientMatchCallback callback) {
         // See if any entries did not resolve; if so, we need to check other
         // directories
-        final Set<String> matchesNotFound = new HashSet<String>();
+
         if (recipientEntries.size() < addresses.size()) {
             final List<DirectorySearchParams> paramsList;
             Cursor directoryCursor = null;
@@ -200,7 +221,10 @@
                 }
             }
         }
+    }
 
+    public static void getMatchingRecipientsFromExtensionMatcher(BaseRecipientAdapter adapter,
+            Set<String> matchesNotFound, RecipientMatchCallback callback) {
         // If no matches found in contact provider or the directories, try the extension
         // matcher.
         // todo (aalbert): This whole method needs to be in the adapter?
diff --git a/src/com/android/ex/chips/RecipientEditTextView.java b/src/com/android/ex/chips/RecipientEditTextView.java
index 791fa8a..2a5405e 100644
--- a/src/com/android/ex/chips/RecipientEditTextView.java
+++ b/src/com/android/ex/chips/RecipientEditTextView.java
@@ -91,10 +91,8 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -333,7 +331,7 @@
         setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context));
     }
 
-    protected void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
+    public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
         mDropdownChipLayouter = dropdownChipLayouter;
     }
 
@@ -397,6 +395,24 @@
         return last;
     }
 
+    /**
+     * @return The list of {@link RecipientEntry}s that have been selected by the user.
+     */
+    public List<RecipientEntry> getSelectedRecipients() {
+        DrawableRecipientChip[] chips =
+                getText().getSpans(0, getText().length(), DrawableRecipientChip.class);
+        List<RecipientEntry> results = new ArrayList();
+        if (chips == null) {
+            return results;
+        }
+
+        for (DrawableRecipientChip c : chips) {
+            results.add(c.getEntry());
+        }
+
+        return results;
+    }
+
     @Override
     public void onSelectionChanged(int start, int end) {
         // When selection changes, see if it is inside the chips area.
@@ -604,14 +620,19 @@
      */
     private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint) {
         paint.setColor(sSelectedTextColor);
-        Bitmap photo;
-        if (mDisableDelete) {
-            // Show the avatar instead if we don't want to delete
-            photo = getAvatarIcon(contact);
-        } else {
-            photo = ((BitmapDrawable) mChipDelete).getBitmap();
+        final ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint,
+                mChipBackgroundPressed);
+
+        if (bitmapContainer.loadIcon) {
+            if (mDisableDelete) {
+                // Show the avatar instead if we don't want to delete
+                loadAvatarIcon(contact, bitmapContainer, paint);
+            } else {
+                drawIcon(bitmapContainer, ((BitmapDrawable) mChipDelete).getBitmap(), paint);
+            }
         }
-        return createChipBitmap(contact, paint, photo, mChipBackgroundPressed);
+
+        return bitmapContainer.bitmap;
     }
 
     /**
@@ -620,21 +641,27 @@
      * @param contact The recipient entry to pull data from.
      * @param paint The paint to use to draw the bitmap.
      */
-    // TODO: Is leaveBlankIconSpacer obsolete now that we have left and right attributes?
-    private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint,
-            boolean leaveBlankIconSpacer) {
+    private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint) {
         Drawable background = getChipBackground(contact);
-        Bitmap photo = getAvatarIcon(contact);
         paint.setColor(getContext().getResources().getColor(android.R.color.black));
-        return createChipBitmap(contact, paint, photo, background);
+        ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint, background);
+
+        if (bitmapContainer.loadIcon) {
+            loadAvatarIcon(contact, bitmapContainer, paint);
+        }
+        return bitmapContainer.bitmap;
     }
 
-    private Bitmap createChipBitmap(RecipientEntry contact, TextPaint paint, Bitmap icon,
-        Drawable background) {
+    private ChipBitmapContainer createChipBitmap(RecipientEntry contact, TextPaint paint,
+            Drawable background) {
+        final ChipBitmapContainer result = new ChipBitmapContainer();
+
         if (background == null) {
-            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
-            return Bitmap.createBitmap(
+            Log.w(TAG, "Unable to draw a background for the chip as it was never set");
+            result.bitmap = Bitmap.createBitmap(
                     (int) mChipHeight * 2, (int) mChipHeight, Bitmap.Config.ARGB_8888);
+            result.loadIcon = false;
+            return result;
         }
 
         Rect backgroundPadding = new Rect();
@@ -660,8 +687,8 @@
                 + backgroundPadding.left + backgroundPadding.right);
 
         // Create the background of the chip.
-        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(tmpBitmap);
+        result.bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(result.bitmap);
 
         // Draw the background drawable
         background.setBounds(0, 0, width, height);
@@ -672,19 +699,27 @@
                 width - backgroundPadding.right - mChipPadding - textWidth;
         canvas.drawText(ellipsizedText, 0, ellipsizedText.length(),
                 textX, getTextYOffset(ellipsizedText.toString(), paint, height), paint);
-        if (icon != null) {
-            // Draw the icon
-            int iconX = shouldPositionAvatarOnRight() ?
-                    width - backgroundPadding.right - iconWidth :
-                    backgroundPadding.left;
-            RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight());
-            RectF dst = new RectF(iconX,
-                    0 + backgroundPadding.top,
-                    iconX + iconWidth,
-                    height - backgroundPadding.bottom);
-            drawIconOnCanvas(icon, canvas, paint, src, dst);
-        }
-        return tmpBitmap;
+
+        // Set the variables that are needed to draw the icon bitmap once it's loaded
+        int iconX = shouldPositionAvatarOnRight() ? width - backgroundPadding.right - iconWidth :
+                backgroundPadding.left;
+        result.left = iconX;
+        result.top = backgroundPadding.top;
+        result.right = iconX + iconWidth;
+        result.bottom = height - backgroundPadding.bottom;
+
+        return result;
+    }
+
+    /**
+     * Helper function that draws the loaded icon bitmap into the chips bitmap
+     */
+    private void drawIcon(ChipBitmapContainer bitMapResult, Bitmap icon, Paint paint) {
+        final Canvas canvas = new Canvas(bitMapResult.bitmap);
+        final RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight());
+        final RectF dst = new RectF(bitMapResult.left, bitMapResult.top, bitMapResult.right,
+                bitMapResult.bottom);
+        drawIconOnCanvas(icon, canvas, paint, src, dst);
     }
 
     /**
@@ -704,7 +739,8 @@
      * Returns the avatar icon to use for this recipient entry. Returns null if we don't want to
      * draw an icon for this recipient.
      */
-    private Bitmap getAvatarIcon(RecipientEntry contact) {
+    private void loadAvatarIcon(final RecipientEntry contact,
+            final ChipBitmapContainer bitmapContainer, final Paint paint) {
         // Don't draw photos for recipients that have been typed in OR generated on the fly.
         long contactId = contact.getContactId();
         boolean drawPhotos = isPhoneQuery() ?
@@ -714,23 +750,58 @@
                                 !TextUtils.isEmpty(contact.getDisplayName())));
 
         if (drawPhotos) {
-            byte[] photoBytes = contact.getPhotoBytes();
+            final byte[] origPhotoBytes = contact.getPhotoBytes();
             // There may not be a photo yet if anything but the first contact address
             // was selected.
-            if (photoBytes == null && contact.getPhotoThumbnailUri() != null) {
+            if (origPhotoBytes == null) {
                 // TODO: cache this in the recipient entry?
-                getAdapter().fetchPhoto(contact, contact.getPhotoThumbnailUri());
-                photoBytes = contact.getPhotoBytes();
-            }
-            if (photoBytes != null) {
-                return BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
+                getAdapter().fetchPhoto(contact, new PhotoManager.PhotoManagerCallback() {
+                    @Override
+                    public void onPhotoBytesPopulated() {
+                        // Call through to the async version which will ensure
+                        // proper threading.
+                        onPhotoBytesAsynchronouslyPopulated();
+                    }
+
+                    @Override
+                    public void onPhotoBytesAsynchronouslyPopulated() {
+                        final byte[] loadedPhotoBytes = contact.getPhotoBytes();
+                        final Bitmap icon = BitmapFactory.decodeByteArray(loadedPhotoBytes, 0,
+                                loadedPhotoBytes.length);
+                        tryDrawAndInvalidate(icon);
+                    }
+
+                    @Override
+                    public void onPhotoBytesAsyncLoadFailed() {
+                        // TODO: can the scaled down default photo be cached?
+                        tryDrawAndInvalidate(mDefaultContactPhoto);
+                    }
+
+                    private void tryDrawAndInvalidate(Bitmap icon) {
+                        drawIcon(bitmapContainer, icon, paint);
+                        // The caller might originated from a background task. However, if the
+                        // background task has already completed, the view might be already drawn
+                        // on the UI but the callback would happen on the background thread.
+                        // So if we are on a background thread, post an invalidate call to the UI.
+                        if (Looper.myLooper() == Looper.getMainLooper()) {
+                            // The view might not redraw itself since it's loaded asynchronously
+                            invalidate();
+                        } else {
+                            post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    invalidate();
+                                }
+                            });
+                        }
+                    }
+                });
             } else {
-                // TODO: can the scaled down default photo be cached?
-                return mDefaultContactPhoto;
+                final Bitmap icon = BitmapFactory.decodeByteArray(origPhotoBytes, 0,
+                        origPhotoBytes.length);
+                drawIcon(bitmapContainer, icon, paint);
             }
         }
-
-        return null;
     }
 
     /**
@@ -761,8 +832,8 @@
         canvas.drawBitmap(icon, matrix, paint);
     }
 
-    private DrawableRecipientChip constructChipSpan(RecipientEntry contact, boolean pressed,
-            boolean leaveIconSpace) throws NullPointerException {
+    private DrawableRecipientChip constructChipSpan(RecipientEntry contact, boolean pressed)
+            throws NullPointerException {
         if (mChipBackground == null) {
             throw new NullPointerException(
                     "Unable to render any chips as setChipDimensions was not called.");
@@ -777,7 +848,7 @@
             tmpBitmap = createSelectedChip(contact, paint);
 
         } else {
-            tmpBitmap = createUnselectedChip(contact, paint, leaveIconSpace);
+            tmpBitmap = createUnselectedChip(contact, paint);
         }
 
         // Pass the full text, un-ellipsized, to the chip.
@@ -1084,16 +1155,8 @@
             DrawableRecipientChip chip = null;
             try {
                 if (!mNoChips) {
-                    /*
-                     * leave space for the contact icon if this is not just an
-                     * email address
-                     */
-                    boolean leaveSpace = TextUtils.isEmpty(entry.getDisplayName())
-                            || TextUtils.equals(entry.getDisplayName(),
-                                    entry.getDestination());
                     chip = visible ?
-                            constructChipSpan(entry, false, leaveSpace)
-                            : new InvisibleRecipientChip(entry);
+                            constructChipSpan(entry, false) : new InvisibleRecipientChip(entry);
                 }
             } catch (NullPointerException e) {
                 Log.e(TAG, e.getMessage(), e);
@@ -1260,9 +1323,9 @@
 
     /**
      * Create a chip from the default selection. If the popup is showing, the
-     * default is the first item in the popup suggestions list. Otherwise, it is
-     * whatever the user had typed in. End represents where the the tokenizer
-     * should search for a token to turn into a chip.
+     * default is the selected item (if one is selected), or the first item, in the popup
+     * suggestions list. Otherwise, it is whatever the user had typed in. End represents where the
+     * tokenizer should search for a token to turn into a chip.
      * @return If a chip was created from a real contact.
      */
     private boolean commitDefault() {
@@ -1306,11 +1369,17 @@
         ListAdapter adapter = getAdapter();
         if (adapter != null && adapter.getCount() > 0 && enoughToFilter()
                 && end == getSelectionEnd() && !isPhoneQuery()) {
-            // let's choose the first entry if only the input text is NOT an email address
-            // so we won't try to replace the user's potentially correct but new/unencountered
-            // email input
+            // let's choose the selected or first entry if only the input text is NOT an email
+            // address so we won't try to replace the user's potentially correct but
+            // new/unencountered email input
             if (!isValidEmailAddress(editable.toString().substring(start, end).trim())) {
-                submitItemAtPosition(0);
+                final int selectedPosition = getListSelection();
+                if (selectedPosition == -1) {
+                    // Nothing is selected; use the first item
+                    submitItemAtPosition(0);
+                } else {
+                    submitItemAtPosition(selectedPosition);
+                }
             }
             dismissDropDown();
             return true;
@@ -1759,8 +1828,7 @@
         chipText = new SpannableString(displayText);
         if (!mNoChips) {
             try {
-                DrawableRecipientChip chip = constructChipSpan(entry, pressed,
-                        false /* leave space for contact icon */);
+                DrawableRecipientChip chip = constructChipSpan(entry, pressed);
                 chipText.setSpan(chip, 0, textLength,
                         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                 chip.setOriginalText(chipText.toString());
@@ -1769,10 +1837,17 @@
                 return null;
             }
         }
+        onChipCreated(entry);
         return chipText;
     }
 
     /**
+     * A callback for subclasses to use to know when a chip was created with the
+     * given RecipientEntry.
+     */
+    protected void onChipCreated(RecipientEntry entry) {}
+
+    /**
      * When an item in the suggestions list has been clicked, create a chip from the
      * contact information of the selected item.
      */
@@ -1833,31 +1908,6 @@
         return entry;
     }
 
-    /** Returns a collection of contact Id for each chip inside this View. */
-    /* package */ Collection<Long> getContactIds() {
-        final Set<Long> result = new HashSet<Long>();
-        DrawableRecipientChip[] chips = getSortedRecipients();
-        if (chips != null) {
-            for (DrawableRecipientChip chip : chips) {
-                result.add(chip.getContactId());
-            }
-        }
-        return result;
-    }
-
-
-    /** Returns a collection of data Id for each chip inside this View. May be null. */
-    /* package */ Collection<Long> getDataIds() {
-        final Set<Long> result = new HashSet<Long>();
-        DrawableRecipientChip [] chips = getSortedRecipients();
-        if (chips != null) {
-            for (DrawableRecipientChip chip : chips) {
-                result.add(chip.getDataId());
-            }
-        }
-        return result;
-    }
-
     // Visible for testing.
     /* package */DrawableRecipientChip[] getSortedRecipients() {
         DrawableRecipientChip[] recips = getSpannable()
@@ -2101,42 +2151,20 @@
             editable.append(text);
             return constructChipSpan(
                     RecipientEntry.constructFakeEntry((String) text, isValid(text.toString())),
-                    true, false);
-        } else if (currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT) {
-            int start = getChipStart(currentChip);
-            int end = getChipEnd(currentChip);
-            getSpannable().removeSpan(currentChip);
-            DrawableRecipientChip newChip;
-            try {
-                if (mNoChips) {
-                    return null;
-                }
-                newChip = constructChipSpan(currentChip.getEntry(), true, false);
-            } catch (NullPointerException e) {
-                Log.e(TAG, e.getMessage(), e);
-                return null;
-            }
-            Editable editable = getText();
-            QwertyKeyListener.markAsReplaced(editable, start, end, "");
-            if (start == -1 || end == -1) {
-                Log.d(TAG, "The chip being selected no longer exists but should.");
-            } else {
-                editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            }
-            newChip.setSelected(true);
-            if (shouldShowEditableText(newChip)) {
-                scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
-            }
-            showAddress(newChip, mAddressPopup, getWidth());
-            setCursorVisible(false);
-            return newChip;
+                    true);
         } else {
             int start = getChipStart(currentChip);
             int end = getChipEnd(currentChip);
             getSpannable().removeSpan(currentChip);
             DrawableRecipientChip newChip;
+            final boolean showAddress =
+                    currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT ||
+                    getAdapter().forceShowAddress();
             try {
-                newChip = constructChipSpan(currentChip.getEntry(), true, false);
+                if (showAddress && mNoChips) {
+                    return null;
+                }
+                newChip = constructChipSpan(currentChip.getEntry(), true);
             } catch (NullPointerException e) {
                 Log.e(TAG, e.getMessage(), e);
                 return null;
@@ -2152,7 +2180,11 @@
             if (shouldShowEditableText(newChip)) {
                 scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
             }
-            showAlternates(newChip, mAlternatesPopup, getWidth());
+            if (showAddress) {
+                showAddress(newChip, mAddressPopup, getWidth());
+            } else {
+                showAlternates(newChip, mAlternatesPopup, getWidth());
+            }
             setCursorVisible(false);
             return newChip;
         }
@@ -2210,7 +2242,7 @@
             editable.removeSpan(chip);
             try {
                 if (!mNoChips) {
-                    editable.setSpan(constructChipSpan(chip.getEntry(), false, false),
+                    editable.setSpan(constructChipSpan(chip.getEntry(), false),
                             start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                 }
             } catch (NullPointerException e) {
@@ -2587,8 +2619,7 @@
                 if (mNoChips) {
                     return null;
                 }
-                return constructChipSpan(entry, false,
-                        false /*leave space for contact icon */);
+                return constructChipSpan(entry, false);
             } catch (NullPointerException e) {
                 Log.e(TAG, e.getMessage(), e);
                 return null;
@@ -2650,8 +2681,7 @@
                 }
             }
             final BaseRecipientAdapter adapter = getAdapter();
-            RecipientAlternatesAdapter.getMatchingRecipients(getContext(), adapter, addresses,
-                    adapter.getAccount(), new RecipientMatchCallback() {
+            adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
                         @Override
                         public void matchesFound(Map<String, RecipientEntry> entries) {
                             final ArrayList<DrawableRecipientChip> replacements =
@@ -2778,9 +2808,7 @@
                 }
             }
             final BaseRecipientAdapter adapter = getAdapter();
-            RecipientAlternatesAdapter.getMatchingRecipients(getContext(), adapter, addresses,
-                    adapter.getAccount(),
-                    new RecipientMatchCallback() {
+            adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
 
                         @Override
                         public void matchesFound(Map<String, RecipientEntry> entries) {
@@ -2931,7 +2959,7 @@
     }
 
     /**
-     * Drag shadow for a {@link RecipientChip}.
+     * Drag shadow for a {@link DrawableRecipientChip}.
      */
     private final class RecipientChipShadow extends DragShadowBuilder {
         private final DrawableRecipientChip mChip;
@@ -3016,4 +3044,55 @@
     public BaseRecipientAdapter getAdapter() {
         return (BaseRecipientAdapter) super.getAdapter();
     }
+
+    /**
+     * Append a new {@link RecipientEntry} to the end of the recipient chips, leaving any
+     * unfinished text at the end.
+     */
+    public void appendRecipientEntry(final RecipientEntry entry) {
+        clearComposingText();
+
+        final Editable editable = getText();
+        int chipInsertionPoint = 0;
+
+        // Find the end of last chip and see if there's any unchipified text.
+        final DrawableRecipientChip[] recips = getSortedRecipients();
+        if (recips != null && recips.length > 0) {
+            final DrawableRecipientChip last = recips[recips.length - 1];
+            // The chip will be inserted at the end of last chip + 1. All the unfinished text after
+            // the insertion point will be kept untouched.
+            chipInsertionPoint = editable.getSpanEnd(last) + 1;
+        }
+
+        final CharSequence chip = createChip(entry, false);
+        if (chip != null) {
+            editable.insert(chipInsertionPoint, chip);
+        }
+    }
+
+    /**
+     * Remove all chips matching the given RecipientEntry.
+     */
+    public void removeRecipientEntry(final RecipientEntry entry) {
+        final DrawableRecipientChip[] recips = getText()
+                .getSpans(0, getText().length(), DrawableRecipientChip.class);
+
+        for (final DrawableRecipientChip recipient : recips) {
+            final RecipientEntry existingEntry = recipient.getEntry();
+            if (existingEntry != null && existingEntry.isValid() &&
+                    existingEntry.isSamePerson(entry)) {
+                removeChip(recipient);
+            }
+        }
+    }
+
+    private static class ChipBitmapContainer {
+        Bitmap bitmap;
+        // information used for positioning the loaded icon
+        boolean loadIcon = true;
+        float left;
+        float top;
+        float right;
+        float bottom;
+    }
 }
diff --git a/src/com/android/ex/chips/RecipientEntry.java b/src/com/android/ex/chips/RecipientEntry.java
index 7d9b87f..c81202e 100644
--- a/src/com/android/ex/chips/RecipientEntry.java
+++ b/src/com/android/ex/chips/RecipientEntry.java
@@ -35,7 +35,7 @@
     /* package */ static final int GENERATED_CONTACT = -2;
 
     /** Used when {@link #mDestinationType} is invalid and thus shouldn't be used for display. */
-    /* package */ static final int INVALID_DESTINATION_TYPE = -1;
+    public static final int INVALID_DESTINATION_TYPE = -1;
 
     public static final int ENTRY_TYPE_PERSON = 0;
 
@@ -76,10 +76,10 @@
      */
     private byte[] mPhotoBytes;
 
-    /** See {@link ContactsContract.Contacts#LOOKUP_KEY} */
+    /** See {@link android.provider.ContactsContract.ContactsColumns#LOOKUP_KEY} */
     private final String mLookupKey;
 
-    private RecipientEntry(int entryType, String displayName, String destination,
+    protected RecipientEntry(int entryType, String displayName, String destination,
             int destinationType, String destinationLabel, long contactId, Long directoryId,
             long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid,
             String lookupKey) {
@@ -174,7 +174,7 @@
         return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
                 displayName, destination), destination, destinationType, destinationLabel,
                 contactId, directoryId, dataId, (thumbnailUriAsString != null
-                        ? Uri.parse(thumbnailUriAsString) : null), true, isValid, lookupKey);
+                ? Uri.parse(thumbnailUriAsString) : null), true, isValid, lookupKey);
     }
 
     public static RecipientEntry constructSecondLevelEntry(String displayName,
@@ -184,7 +184,7 @@
         return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
                 displayName, destination), destination, destinationType, destinationLabel,
                 contactId, directoryId, dataId, (thumbnailUriAsString != null
-                        ? Uri.parse(thumbnailUriAsString) : null), false, isValid, lookupKey);
+                ? Uri.parse(thumbnailUriAsString) : null), false, isValid, lookupKey);
     }
 
     public int getEntryType() {
@@ -253,4 +253,12 @@
     public String toString() {
         return mDisplayName + " <" + mDestination + ">, isValid=" + mIsValid;
     }
+
+    /**
+     * Returns if entry represents the same person as this instance. The default implementation
+     * checks whether the contact ids are the same, and subclasses may opt to override this.
+     */
+    public boolean isSamePerson(final RecipientEntry entry) {
+        return entry != null && mContactId == entry.mContactId;
+    }
 }
\ No newline at end of file