am 8af0d3b6: Allow support for alternate data sources.
* commit '8af0d3b6f34e03c08c8e67be2190da01c59889da':
Allow support for alternate data sources.
diff --git a/src/com/android/ex/chips/BaseRecipientAdapter.java b/src/com/android/ex/chips/BaseRecipientAdapter.java
index b1045a4..6b37359 100644
--- a/src/com/android/ex/chips/BaseRecipientAdapter.java
+++ b/src/com/android/ex/chips/BaseRecipientAdapter.java
@@ -23,16 +23,11 @@
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;
@@ -45,10 +40,6 @@
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;
@@ -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.
@@ -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();
@@ -352,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.
*/
@@ -433,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);
}
}
@@ -457,14 +438,14 @@
}
// Show the list again without "waiting" message.
- updateEntries(constructEntryList(mEntryMap, mNonAggregatedEntries));
+ updateEntries(constructEntryList());
}
}
private final Context mContext;
private final ContentResolver mContentResolver;
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());
}
}
@@ -554,7 +538,7 @@
mContext = context;
mContentResolver = context.getContentResolver();
mPreferredMaxResultCount = preferredMaxResultCount;
- mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE);
+ mPhotoManager = new DefaultPhotoManager(mContentResolver);
mQueryType = queryMode;
if (queryMode == QUERY_TYPE_EMAIL) {
@@ -585,6 +569,50 @@
}
/**
+ * Enables overriding the default photo manager that is used.
+ */
+ public void setPhotoManager(PhotoManager photoManager) {
+ mPhotoManager = photoManager;
+ }
+
+ public PhotoManager getPhotoManager() {
+ return mPhotoManager;
+ }
+
+ /**
+ * If true, a null thumbnail uri is ignored when trying to query for a photo.
+ * Derived classes should only return true if a {@link com.android.ex.chips.PhotoManager}
+ * is used that does not rely on thumbnail uris. Default implementation returns {@code false}.
+ * @return
+ */
+ public boolean ignoreNullThumbnailUri() {
+ return false;
+ }
+
+ /**
+ * 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
@@ -599,7 +627,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
@@ -686,6 +714,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,
@@ -725,6 +767,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.
@@ -740,7 +791,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) {
@@ -753,8 +804,7 @@
break;
}
entries.add(entry);
- tryFetchPhoto(entry);
-
+ mPhotoManager.populatePhotoBytesAsync(entry, this);
validEntryCount++;
}
}
@@ -772,17 +822,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;
}
@@ -790,135 +840,8 @@
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);
- }
- }
- }
+ mPhotoManager.populatePhotoBytesSync(entry);
}
private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) {
@@ -1001,4 +924,9 @@
public Account getAccount() {
return mAccount;
}
+
+ @Override
+ public void onPhotoBytesAsynchronouslyPopulated() {
+ notifyDataSetChanged();
+ }
}
diff --git a/src/com/android/ex/chips/DefaultPhotoManager.java b/src/com/android/ex/chips/DefaultPhotoManager.java
new file mode 100644
index 0000000..04b772e
--- /dev/null
+++ b/src/com/android/ex/chips/DefaultPhotoManager.java
@@ -0,0 +1,202 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+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.FileNotFoundException;
+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);
+ // 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, callback);
+ }
+ }
+ }
+
+ 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();
+ }
+ }
+ }
+ };
+ photoLoadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+
+ @Override
+ public void populatePhotoBytesSync(RecipientEntry entry) {
+ final Uri photoThumbnailUri = entry.getPhotoThumbnailUri();
+ if (photoThumbnailUri == null) {
+ return;
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/ex/chips/PhotoManager.java b/src/com/android/ex/chips/PhotoManager.java
new file mode 100644
index 0000000..8cb8df6
--- /dev/null
+++ b/src/com/android/ex/chips/PhotoManager.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+ void populatePhotoBytesAsync(RecipientEntry entry, PhotoManagerCallback callback);
+
+ /**
+ * Sets the {@link com.android.ex.chips.RecipientEntry}'s photo bytes. All work
+ * is performed synchronously.
+ */
+ void populatePhotoBytesSync(RecipientEntry entry);
+
+ interface PhotoManagerCallback {
+ void onPhotoBytesAsynchronouslyPopulated();
+ }
+}
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 7eaee98..1d3a167 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;
@@ -321,7 +319,7 @@
setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context));
}
- protected void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
+ public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
mDropdownChipLayouter = dropdownChipLayouter;
}
@@ -717,7 +715,8 @@
byte[] photoBytes = 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 (photoBytes == null && (contact.getPhotoThumbnailUri() != null ||
+ getAdapter().ignoreNullThumbnailUri())) {
// TODO: cache this in the recipient entry?
getAdapter().fetchPhoto(contact, contact.getPhotoThumbnailUri());
photoBytes = contact.getPhotoBytes();
@@ -1823,31 +1822,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()
@@ -2092,13 +2066,16 @@
return constructChipSpan(
RecipientEntry.constructFakeEntry((String) text, isValid(text.toString())),
true);
- } else if (currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT) {
+ } 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 {
- if (mNoChips) {
+ if (showAddress && mNoChips) {
return null;
}
newChip = constructChipSpan(currentChip.getEntry(), true);
@@ -2117,32 +2094,11 @@
if (shouldShowEditableText(newChip)) {
scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
}
- showAddress(newChip, mAddressPopup, getWidth());
- setCursorVisible(false);
- return newChip;
- } else {
- int start = getChipStart(currentChip);
- int end = getChipEnd(currentChip);
- getSpannable().removeSpan(currentChip);
- DrawableRecipientChip newChip;
- try {
- newChip = constructChipSpan(currentChip.getEntry(), true);
- } 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.");
+ if (showAddress) {
+ showAddress(newChip, mAddressPopup, getWidth());
} else {
- editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ showAlternates(newChip, mAlternatesPopup, getWidth());
}
- newChip.setSelected(true);
- if (shouldShowEditableText(newChip)) {
- scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
- }
- showAlternates(newChip, mAlternatesPopup, getWidth());
setCursorVisible(false);
return newChip;
}
@@ -2639,8 +2595,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 =
@@ -2767,9 +2722,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) {
diff --git a/src/com/android/ex/chips/RecipientEntry.java b/src/com/android/ex/chips/RecipientEntry.java
index 7d9b87f..2afa859 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() {