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