summaryrefslogtreecommitdiffstats
path: root/chips/src/com/android/ex
diff options
context:
space:
mode:
Diffstat (limited to 'chips/src/com/android/ex')
-rw-r--r--chips/src/com/android/ex/chips/BaseRecipientAdapter.java56
-rw-r--r--chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java256
-rw-r--r--chips/src/com/android/ex/chips/RecipientEditTextView.java793
-rw-r--r--chips/src/com/android/ex/chips/RecipientEntry.java118
-rw-r--r--chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java4
-rw-r--r--chips/src/com/android/ex/chips/recipientchip/BaseRecipientChip.java73
-rw-r--r--chips/src/com/android/ex/chips/recipientchip/DrawableRecipientChip.java36
-rw-r--r--chips/src/com/android/ex/chips/recipientchip/InvisibleRecipientChip.java105
-rw-r--r--chips/src/com/android/ex/chips/recipientchip/SimpleRecipientChip.java (renamed from chips/src/com/android/ex/chips/RecipientChip.java)69
-rw-r--r--chips/src/com/android/ex/chips/recipientchip/VisibleRecipientChip.java99
10 files changed, 1120 insertions, 489 deletions
diff --git a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java
index c981728..71b610e 100644
--- a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java
+++ b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java
@@ -73,12 +73,12 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
* The number of extra entries requested to allow for duplicates. Duplicates
* are removed from the overall result.
*/
- private static final int ALLOWANCE_FOR_DUPLICATES = 5;
+ static final int ALLOWANCE_FOR_DUPLICATES = 5;
// This is ContactsContract.PRIMARY_ACCOUNT_NAME. Available from ICS as hidden
- private static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account";
+ static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account";
// This is ContactsContract.PRIMARY_ACCOUNT_TYPE. Available from ICS as hidden
- private static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account";
+ 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;
@@ -118,7 +118,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
public static final int PHOTO = 0;
}
- private static class DirectoryListQuery {
+ protected static class DirectoryListQuery {
public static final Uri URI =
Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories");
@@ -234,8 +234,8 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
}
// We'll copy this result to mEntry in publicResults() (run in the UX thread).
- final List<RecipientEntry> entries = constructEntryList(false,
- entryMap, nonAggregatedEntries, existingDestinations);
+ 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.
@@ -250,7 +250,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
directoryCursor = mContentResolver.query(
DirectoryListQuery.URI, DirectoryListQuery.PROJECTION,
null, null, null);
- paramsList = setupOtherDirectories(directoryCursor);
+ paramsList = setupOtherDirectories(mContext, directoryCursor, mAccount);
} else {
// We don't need to search other directories.
paramsList = null;
@@ -424,8 +424,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
}
// Show the list again without "waiting" message.
- updateEntries(constructEntryList(false,
- mEntryMap, mNonAggregatedEntries, mExistingDestinations));
+ updateEntries(constructEntryList(mEntryMap, mNonAggregatedEntries));
}
}
@@ -482,8 +481,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
@Override
public void handleMessage(Message msg) {
if (mRemainingDirectoryCount > 0) {
- updateEntries(constructEntryList(true,
- mEntryMap, mNonAggregatedEntries, mExistingDestinations));
+ updateEntries(constructEntryList(mEntryMap, mNonAggregatedEntries));
}
}
@@ -556,8 +554,9 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
return new DefaultFilter();
}
- private List<DirectorySearchParams> setupOtherDirectories(Cursor directoryCursor) {
- final PackageManager packageManager = mContext.getPackageManager();
+ public static List<DirectorySearchParams> setupOtherDirectories(Context context,
+ Cursor directoryCursor, Account account) {
+ final PackageManager packageManager = context.getPackageManager();
final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>();
DirectorySearchParams preferredDirectory = null;
while (directoryCursor.moveToNext()) {
@@ -594,8 +593,8 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
// If an account has been provided and we found a directory that
// corresponds to that account, place that directory second, directly
// underneath the local contacts.
- if (mAccount != null && mAccount.name.equals(params.accountName) &&
- mAccount.type.equals(params.accountType)) {
+ if (account != null && account.name.equals(params.accountName) &&
+ account.type.equals(params.accountType)) {
preferredDirectory = params;
} else {
paramsList.add(params);
@@ -633,7 +632,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
mDelayedMessageHandler.sendDelayedLoadMessage();
}
- private void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry,
+ private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry,
LinkedHashMap<Long, List<RecipientEntry>> entryMap,
List<RecipientEntry> nonAggregatedEntries,
Set<String> existingDestinations) {
@@ -648,7 +647,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
entry.displayName,
entry.displayNameSource,
entry.destination, entry.destinationType, entry.destinationLabel,
- entry.contactId, entry.dataId, entry.thumbnailUriString));
+ entry.contactId, entry.dataId, entry.thumbnailUriString, true));
} else if (entryMap.containsKey(entry.contactId)) {
// We already have a section for the person.
final List<RecipientEntry> entryList = entryMap.get(entry.contactId);
@@ -656,14 +655,14 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
entry.displayName,
entry.displayNameSource,
entry.destination, entry.destinationType, entry.destinationLabel,
- entry.contactId, entry.dataId, entry.thumbnailUriString));
+ entry.contactId, entry.dataId, entry.thumbnailUriString, true));
} else {
final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>();
entryList.add(RecipientEntry.constructTopLevelEntry(
entry.displayName,
entry.displayNameSource,
entry.destination, entry.destinationType, entry.destinationLabel,
- entry.contactId, entry.dataId, entry.thumbnailUriString));
+ entry.contactId, entry.dataId, entry.thumbnailUriString, true));
entryMap.put(entry.contactId, entryList);
}
}
@@ -674,10 +673,8 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
* thread to get one from directories.
*/
private List<RecipientEntry> constructEntryList(
- boolean showMessageIfDirectoryLoadRemaining,
LinkedHashMap<Long, List<RecipientEntry>> entryMap,
- List<RecipientEntry> nonAggregatedEntries,
- Set<String> existingDestinations) {
+ List<RecipientEntry> nonAggregatedEntries) {
final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
int validEntryCount = 0;
for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) {
@@ -708,6 +705,11 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
return entries;
}
+
+ protected interface EntriesUpdatedObserver {
+ public void onChanged(List<RecipientEntry> entries);
+ }
+
public void registerUpdateObserver(EntriesUpdatedObserver observer) {
mEntriesUpdatedObserver = observer;
}
@@ -906,7 +908,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
if (imageView != null) {
imageView.setVisibility(View.VISIBLE);
final byte[] photoBytes = entry.getPhotoBytes();
- if (photoBytes != null && imageView != null) {
+ if (photoBytes != null) {
final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
photoBytes.length);
imageView.setImageBitmap(photo);
@@ -975,11 +977,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter
return android.R.id.icon;
}
- /**
- * Interface called before the BaseRecipientAdapter updates recipient
- * results in the popup window.
- */
- protected interface EntriesUpdatedObserver {
- public void onChanged(List<RecipientEntry> entries);
+ public Account getAccount() {
+ return mAccount;
}
}
diff --git a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java b/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java
index 553890e..0693df2 100644
--- a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java
+++ b/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java
@@ -16,9 +16,14 @@
package com.android.ex.chips;
+import android.accounts.Account;
+import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.util.Log;
@@ -29,11 +34,16 @@ import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.ex.chips.BaseRecipientAdapter.DirectoryListQuery;
+import com.android.ex.chips.BaseRecipientAdapter.DirectorySearchParams;
import com.android.ex.chips.Queries.Query;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* RecipientAlternatesAdapter backs the RecipientEditTextView for managing contacts
@@ -55,9 +65,17 @@ public class RecipientAlternatesAdapter extends CursorAdapter {
public static final int QUERY_TYPE_PHONE = 1;
private Query mQuery;
- public static HashMap<String, RecipientEntry> getMatchingRecipients(Context context,
- ArrayList<String> inAddresses) {
- return getMatchingRecipients(context, inAddresses, QUERY_TYPE_EMAIL);
+ public interface RecipientMatchCallback {
+ public void matchesFound(Map<String, RecipientEntry> results);
+ /**
+ * Called with all addresses that could not be resolved to valid recipients.
+ */
+ public void matchesNotFound(Set<String> unfoundAddresses);
+ }
+
+ public static void getMatchingRecipients(Context context, ArrayList<String> inAddresses,
+ Account account, RecipientMatchCallback callback) {
+ getMatchingRecipients(context, inAddresses, QUERY_TYPE_EMAIL, account, callback);
}
/**
@@ -67,10 +85,11 @@ public class RecipientAlternatesAdapter extends CursorAdapter {
*
* @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 HashMap<String, RecipientEntry> getMatchingRecipients(Context context,
- ArrayList<String> inAddresses, int addressType) {
+ public static void getMatchingRecipients(Context context, ArrayList<String> inAddresses,
+ int addressType, Account account, RecipientMatchCallback callback) {
Queries.Query query;
if (addressType == QUERY_TYPE_EMAIL) {
query = Queries.EMAIL;
@@ -78,12 +97,12 @@ public class RecipientAlternatesAdapter extends CursorAdapter {
query = Queries.PHONE;
}
int addressesSize = Math.min(MAX_LOOKUPS, inAddresses.size());
- String[] addresses = new String[addressesSize];
+ HashSet<String> addresses = new HashSet<String>();
StringBuilder bindString = new StringBuilder();
// Create the "?" string and set up arguments.
for (int i = 0; i < addressesSize; i++) {
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase());
- addresses[i] = (tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
+ addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
bindString.append("?");
if (i < addressesSize - 1) {
bindString.append(",");
@@ -94,49 +113,205 @@ public class RecipientAlternatesAdapter extends CursorAdapter {
Log.d(TAG, "Doing reverse lookup for " + addresses.toString());
}
- HashMap<String, RecipientEntry> recipientEntries = new HashMap<String, RecipientEntry>();
- Cursor c = context.getContentResolver().query(
- query.getContentUri(),
- query.getProjection(),
- query.getProjection()[Queries.Query.DESTINATION] + " IN (" + bindString.toString()
- + ")", addresses, null);
-
- if (c != null) {
+ String[] addressArray = new String[addresses.size()];
+ addresses.toArray(addressArray);
+ HashMap<String, RecipientEntry> recipientEntries = null;
+ Cursor c = null;
+
+ try {
+ c = context.getContentResolver().query(
+ query.getContentUri(),
+ query.getProjection(),
+ query.getProjection()[Queries.Query.DESTINATION] + " IN ("
+ + bindString.toString() + ")", addressArray, null);
+ recipientEntries = processContactEntries(c);
+ callback.matchesFound(recipientEntries);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ // 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;
try {
- if (c.moveToFirst()) {
- do {
- String address = c.getString(Queries.Query.DESTINATION);
- recipientEntries.put(address, RecipientEntry.constructTopLevelEntry(
- c.getString(Queries.Query.NAME),
- c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),
- c.getString(Queries.Query.DESTINATION),
- c.getInt(Queries.Query.DESTINATION_TYPE),
- c.getString(Queries.Query.DESTINATION_LABEL),
- c.getLong(Queries.Query.CONTACT_ID),
- c.getLong(Queries.Query.DATA_ID),
- c.getString(Queries.Query.PHOTO_THUMBNAIL_URI)));
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Received reverse look up information for " + address
- + " RESULTS: "
- + " NAME : " + c.getString(Queries.Query.NAME)
- + " CONTACT ID : " + c.getLong(Queries.Query.CONTACT_ID)
- + " ADDRESS :" + c.getString(Queries.Query.DESTINATION));
+ directoryCursor = context.getContentResolver().query(DirectoryListQuery.URI,
+ DirectoryListQuery.PROJECTION, null, null, null);
+ paramsList = BaseRecipientAdapter.setupOtherDirectories(context, directoryCursor,
+ account);
+ } finally {
+ if (directoryCursor != null) {
+ directoryCursor.close();
+ }
+ }
+ // Run a directory query for each unmatched recipient.
+ HashSet<String> unresolvedAddresses = new HashSet<String>();
+ for (String address : addresses) {
+ if (!recipientEntries.containsKey(address)) {
+ unresolvedAddresses.add(address);
+ }
+ }
+
+ matchesNotFound.addAll(unresolvedAddresses);
+
+ Cursor directoryContactsCursor = null;
+ for (String unresolvedAddress : unresolvedAddresses) {
+ for (int i = 0; i < paramsList.size(); i++) {
+ try {
+ directoryContactsCursor = doQuery(unresolvedAddress, 1,
+ paramsList.get(i).directoryId, account,
+ context.getContentResolver(), query);
+ } finally {
+ if (directoryContactsCursor != null
+ && directoryContactsCursor.getCount() == 0) {
+ directoryContactsCursor.close();
+ directoryContactsCursor = null;
+ } else {
+ break;
}
- } while (c.moveToNext());
+ }
+ }
+ if (directoryContactsCursor != null) {
+ try {
+ final Map<String, RecipientEntry> entries =
+ processContactEntries(directoryContactsCursor);
+
+ for (final String address : entries.keySet()) {
+ matchesNotFound.remove(address);
+ }
+
+ callback.matchesFound(entries);
+ } finally {
+ directoryContactsCursor.close();
+ }
}
- } finally {
- c.close();
}
}
+
+ callback.matchesNotFound(matchesNotFound);
+ }
+
+ private static HashMap<String, RecipientEntry> processContactEntries(Cursor c) {
+ HashMap<String, RecipientEntry> recipientEntries = new HashMap<String, RecipientEntry>();
+ if (c != null && c.moveToFirst()) {
+ do {
+ String address = c.getString(Queries.Query.DESTINATION);
+
+ final RecipientEntry newRecipientEntry = RecipientEntry.constructTopLevelEntry(
+ c.getString(Queries.Query.NAME),
+ c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),
+ c.getString(Queries.Query.DESTINATION),
+ c.getInt(Queries.Query.DESTINATION_TYPE),
+ c.getString(Queries.Query.DESTINATION_LABEL),
+ c.getLong(Queries.Query.CONTACT_ID),
+ c.getLong(Queries.Query.DATA_ID),
+ c.getString(Queries.Query.PHOTO_THUMBNAIL_URI),
+ true);
+
+ /*
+ * In certain situations, we may have two results for one address, where one of the
+ * results is just the email address, and the other has a name and photo, so we want
+ * to use the better one.
+ */
+ final RecipientEntry recipientEntry =
+ getBetterRecipient(recipientEntries.get(address), newRecipientEntry);
+
+ recipientEntries.put(address, recipientEntry);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Received reverse look up information for " + address
+ + " RESULTS: "
+ + " NAME : " + c.getString(Queries.Query.NAME)
+ + " CONTACT ID : " + c.getLong(Queries.Query.CONTACT_ID)
+ + " ADDRESS :" + c.getString(Queries.Query.DESTINATION));
+ }
+ } while (c.moveToNext());
+ }
return recipientEntries;
}
- public RecipientAlternatesAdapter(Context context, long contactId, long currentId, int viewId,
+ /**
+ * Given two {@link RecipientEntry}s for the same email address, this will return the one that
+ * contains more complete information for display purposes. Defaults to <code>entry2</code> if
+ * no significant differences are found.
+ */
+ static RecipientEntry getBetterRecipient(final RecipientEntry entry1,
+ final RecipientEntry entry2) {
+ // If only one has passed in, use it
+ if (entry2 == null) {
+ return entry1;
+ }
+
+ if (entry1 == null) {
+ return entry2;
+ }
+
+ // If only one has a display name, use it
+ if (!TextUtils.isEmpty(entry1.getDisplayName())
+ && TextUtils.isEmpty(entry2.getDisplayName())) {
+ return entry1;
+ }
+
+ if (!TextUtils.isEmpty(entry2.getDisplayName())
+ && TextUtils.isEmpty(entry1.getDisplayName())) {
+ return entry2;
+ }
+
+ // If only one has a display name that is not the same as the destination, use it
+ if (!TextUtils.equals(entry1.getDisplayName(), entry1.getDestination())
+ && TextUtils.equals(entry2.getDisplayName(), entry2.getDestination())) {
+ return entry1;
+ }
+
+ if (!TextUtils.equals(entry2.getDisplayName(), entry2.getDestination())
+ && TextUtils.equals(entry1.getDisplayName(), entry1.getDestination())) {
+ return entry2;
+ }
+
+ // If only one has a photo, use it
+ if ((entry1.getPhotoThumbnailUri() != null || entry1.getPhotoBytes() != null)
+ && (entry2.getPhotoThumbnailUri() == null && entry2.getPhotoBytes() == null)) {
+ return entry1;
+ }
+
+ if ((entry2.getPhotoThumbnailUri() != null || entry2.getPhotoBytes() != null)
+ && (entry1.getPhotoThumbnailUri() == null && entry1.getPhotoBytes() == null)) {
+ return entry2;
+ }
+
+ // Go with the second option as a default
+ return entry2;
+ }
+
+ private static Cursor doQuery(CharSequence constraint, int limit, Long directoryId,
+ Account account, ContentResolver resolver, Query query) {
+ final Uri.Builder builder = query
+ .getContentFilterUri()
+ .buildUpon()
+ .appendPath(constraint.toString())
+ .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+ String.valueOf(limit + BaseRecipientAdapter.ALLOWANCE_FOR_DUPLICATES));
+ if (directoryId != null) {
+ builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+ String.valueOf(directoryId));
+ }
+ if (account != null) {
+ builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_NAME, account.name);
+ builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_TYPE, account.type);
+ }
+ final Cursor cursor = resolver.query(builder.build(), query.getProjection(), null, null,
+ null);
+ return cursor;
+ }
+
+ public RecipientAlternatesAdapter(Context context, long contactId, long currentId,
OnCheckedItemChangedListener listener) {
- this(context, contactId, currentId, viewId, QUERY_TYPE_EMAIL, listener);
+ this(context, contactId, currentId, QUERY_TYPE_EMAIL, listener);
}
- public RecipientAlternatesAdapter(Context context, long contactId, long currentId, int viewId,
+ public RecipientAlternatesAdapter(Context context, long contactId, long currentId,
int queryMode, OnCheckedItemChangedListener listener) {
super(context, getCursorForConstruction(context, contactId, queryMode), 0);
mLayoutInflater = LayoutInflater.from(context);
@@ -233,7 +408,8 @@ public class RecipientAlternatesAdapter extends CursorAdapter {
c.getString(Queries.Query.DESTINATION_LABEL),
c.getLong(Queries.Query.CONTACT_ID),
c.getLong(Queries.Query.DATA_ID),
- c.getString(Queries.Query.PHOTO_THUMBNAIL_URI));
+ c.getString(Queries.Query.PHOTO_THUMBNAIL_URI),
+ true);
}
@Override
diff --git a/chips/src/com/android/ex/chips/RecipientEditTextView.java b/chips/src/com/android/ex/chips/RecipientEditTextView.java
index 93ca4e8..d2e5806 100644
--- a/chips/src/com/android/ex/chips/RecipientEditTextView.java
+++ b/chips/src/com/android/ex/chips/RecipientEditTextView.java
@@ -1,4 +1,5 @@
/*
+
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +17,18 @@
package com.android.ex.chips;
+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;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
import android.app.Dialog;
import android.content.ClipData;
import android.content.ClipDescription;
@@ -36,6 +49,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.text.Editable;
@@ -55,7 +69,6 @@ import android.text.util.Rfc822Tokenizer;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
-import android.util.Patterns;
import android.view.ActionMode;
import android.view.ActionMode.Callback;
import android.view.DragEvent;
@@ -81,17 +94,10 @@ import android.widget.MultiAutoCompleteTextView;
import android.widget.ScrollView;
import android.widget.TextView;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
+import com.android.ex.chips.recipientchip.DrawableRecipientChip;
+import com.android.ex.chips.recipientchip.InvisibleRecipientChip;
+import com.android.ex.chips.recipientchip.VisibleRecipientChip;
/**
* RecipientEditTextView is an auto complete text view for use with applications
@@ -104,12 +110,13 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
private static final char COMMIT_CHAR_COMMA = ',';
- private static final char NAME_WRAPPER_CHAR = '"';
-
private static final char COMMIT_CHAR_SEMICOLON = ';';
private static final char COMMIT_CHAR_SPACE = ' ';
+ private static final String SEPARATOR = String.valueOf(COMMIT_CHAR_COMMA)
+ + String.valueOf(COMMIT_CHAR_SPACE);
+
private static final String TAG = "RecipientEditTextView";
private static int DISMISS = "dismiss".hashCode();
@@ -145,7 +152,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
private Validator mValidator;
- private RecipientChip mSelectedChip;
+ private DrawableRecipientChip mSelectedChip;
private int mAlternatesLayout;
@@ -155,7 +162,8 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
private TextView mMoreItem;
- private final ArrayList<String> mPendingChips = new ArrayList<String>();
+ // VisibleForTesting
+ final ArrayList<String> mPendingChips = new ArrayList<String>();
private Handler mHandler;
@@ -167,9 +175,10 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
private ListPopupWindow mAddressPopup;
- private ArrayList<RecipientChip> mTemporaryRecipients;
+ // VisibleForTesting
+ ArrayList<DrawableRecipientChip> mTemporaryRecipients;
- private ArrayList<RecipientChip> mRemovedSpans;
+ private ArrayList<DrawableRecipientChip> mRemovedSpans;
private boolean mShouldShrink = true;
@@ -206,7 +215,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
+ "(1?[ ]*\\([0-9]+\\)[\\- \\.]*)?" // 1(<digits>)<sdd>*
+ "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit>
-
private final Runnable mAddTextWatcher = new Runnable() {
@Override
public void run() {
@@ -282,7 +290,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
addTextChangedListener(mTextWatcher);
mGestureDetector = new GestureDetector(context, this);
setOnEditorActionListener(this);
- mMaxLines = getLineCount();
}
@Override
@@ -314,13 +321,15 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
+
+ outAttrs.actionId = EditorInfo.IME_ACTION_DONE;
outAttrs.actionLabel = getContext().getString(R.string.done);
return connection;
}
- /*package*/ RecipientChip getLastChip() {
- RecipientChip last = null;
- RecipientChip[] chips = getSortedRecipients();
+ /*package*/ DrawableRecipientChip getLastChip() {
+ DrawableRecipientChip last = null;
+ DrawableRecipientChip[] chips = getSortedRecipients();
if (chips != null && chips.length > 0) {
last = chips[chips.length - 1];
}
@@ -331,7 +340,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
public void onSelectionChanged(int start, int end) {
// When selection changes, see if it is inside the chips area.
// If so, move the cursor back after the chips again.
- RecipientChip last = getLastChip();
+ DrawableRecipientChip last = getLastChip();
if (last != null && start < getSpannable().getSpanEnd(last)) {
// Grab the last chip and set the cursor to after it.
setSelection(Math.min(getSpannable().getSpanEnd(last) + 1, getText().length()));
@@ -370,22 +379,17 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
super.append(text, start, end);
if (!TextUtils.isEmpty(text) && TextUtils.getTrimmedLength(text) > 0) {
String displayString = text.toString();
- int separatorPos = displayString.lastIndexOf(COMMIT_CHAR_COMMA);
- // Verify that the separator pos is not within ""; if it is, look
- // past the closing quote. If there is no comma past ", this string
- // will resolve to an error chip.
- if (separatorPos > -1) {
- String parseDisplayString = displayString.substring(separatorPos);
- int endQuotedTextPos = parseDisplayString.indexOf(NAME_WRAPPER_CHAR);
- if (endQuotedTextPos > separatorPos) {
- separatorPos = parseDisplayString.lastIndexOf(COMMIT_CHAR_COMMA,
- endQuotedTextPos);
- }
+
+ if (!displayString.trim().endsWith(String.valueOf(COMMIT_CHAR_COMMA))) {
+ // We have no separator, so we should add it
+ super.append(SEPARATOR, 0, SEPARATOR.length());
+ displayString += SEPARATOR;
}
+
if (!TextUtils.isEmpty(displayString)
&& TextUtils.getTrimmedLength(displayString) > 0) {
mPendingChipsCount++;
- mPendingChips.add(text.toString());
+ mPendingChips.add(displayString);
}
}
// Put a message on the queue to make sure we ALWAYS handle pending
@@ -413,6 +417,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
return sExcessTopPadding;
}
+ @Override
public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
super.setAdapter(adapter);
((BaseRecipientAdapter) adapter)
@@ -476,7 +481,8 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
Editable editable = getText();
int end = getSelectionEnd();
int start = mTokenizer.findTokenStart(editable, end);
- RecipientChip[] chips = getSpannable().getSpans(start, end, RecipientChip.class);
+ DrawableRecipientChip[] chips =
+ getSpannable().getSpans(start, end, DrawableRecipientChip.class);
if ((chips == null || chips.length == 0)) {
Editable text = getText();
int whatEnd = mTokenizer.findTokenEnd(text, start);
@@ -524,7 +530,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
TextUtils.TruncateAt.END);
}
- private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint, Layout layout) {
+ private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint) {
// Ellipsize the text so that it takes AT MOST the entire width of the
// autocomplete text entry area. Make sure to leave space for padding
// on the sides.
@@ -533,7 +539,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
float[] widths = new float[1];
paint.getTextWidths(" ", widths);
CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
- calculateAvailableWidth(true) - deleteWidth - widths[0]);
+ calculateAvailableWidth() - deleteWidth - widths[0]);
// Make sure there is a minimum chip width so the user can ALWAYS
// tap a chip without difficulty.
@@ -566,7 +572,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
- private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint, Layout layout,
+ private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint,
boolean leaveBlankIconSpacer) {
// Ellipsize the text so that it takes AT MOST the entire width of the
// autocomplete text entry area. Make sure to leave space for padding
@@ -576,7 +582,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
float[] widths = new float[1];
paint.getTextWidths(" ", widths);
CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
- calculateAvailableWidth(false) - iconWidth - widths[0]);
+ calculateAvailableWidth() - iconWidth - widths[0]);
// Make sure there is a minimum chip width so the user can ALWAYS
// tap a chip without difficulty.
int width = Math.max(iconWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
@@ -646,25 +652,23 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* Get the background drawable for a RecipientChip.
*/
// Visible for testing.
- /*package*/ Drawable getChipBackground(RecipientEntry contact) {
- return (mValidator != null && mValidator.isValid(contact.getDestination())) ?
- mChipBackground : mInvalidChipBackground;
+ /* package */Drawable getChipBackground(RecipientEntry contact) {
+ return contact.isValid() ? mChipBackground : mInvalidChipBackground;
}
- private float getTextYOffset(String text, TextPaint paint, int height) {
+ private static float getTextYOffset(String text, TextPaint paint, int height) {
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
int textHeight = bounds.bottom - bounds.top ;
return height - ((height - textHeight) / 2) - (int)paint.descent();
}
- private RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed,
+ private DrawableRecipientChip constructChipSpan(RecipientEntry contact, boolean pressed,
boolean leaveIconSpace) throws NullPointerException {
if (mChipBackground == null) {
throw new NullPointerException(
"Unable to render any chips as setChipDimensions was not called.");
}
- Layout layout = getLayout();
TextPaint paint = getPaint();
float defaultSize = paint.getTextSize();
@@ -672,16 +676,16 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
Bitmap tmpBitmap;
if (pressed) {
- tmpBitmap = createSelectedChip(contact, paint, layout);
+ tmpBitmap = createSelectedChip(contact, paint);
} else {
- tmpBitmap = createUnselectedChip(contact, paint, layout, leaveIconSpace);
+ tmpBitmap = createUnselectedChip(contact, paint, leaveIconSpace);
}
// Pass the full text, un-ellipsized, to the chip.
Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
- RecipientChip recipientChip = new RecipientChip(result, contact, offset);
+ DrawableRecipientChip recipientChip = new VisibleRecipientChip(result, contact);
// Return text to the original size.
paint.setTextSize(defaultSize);
paint.setColor(defaultColor);
@@ -706,7 +710,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* account the width of the EditTextView, any view padding, and padding
* that will be added to the chip.
*/
- private float calculateAvailableWidth(boolean pressed) {
+ private float calculateAvailableWidth() {
return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2);
}
@@ -715,6 +719,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecipientEditTextView, 0,
0);
Resources r = getContext().getResources();
+
mChipBackground = a.getDrawable(R.styleable.RecipientEditTextView_chipBackground);
if (mChipBackground == null) {
mChipBackground = r.getDrawable(R.drawable.chip_background);
@@ -755,7 +760,8 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if (mInvalidChipBackground == null) {
mInvalidChipBackground = r.getDrawable(R.drawable.chip_background_invalid);
}
- mLineSpacingExtra = context.getResources().getDimension(R.dimen.line_spacing_extra);
+ mLineSpacingExtra = r.getDimension(R.dimen.line_spacing_extra);
+ mMaxLines = r.getInteger(R.integer.chips_max_lines);
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
mActionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources()
@@ -821,11 +827,11 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
private void checkChipWidths() {
// Check the widths of the associated chips.
- RecipientChip[] chips = getSortedRecipients();
+ DrawableRecipientChip[] chips = getSortedRecipients();
if (chips != null) {
Rect bounds;
- for (RecipientChip chip : chips) {
- bounds = chip.getDrawable().getBounds();
+ for (DrawableRecipientChip chip : chips) {
+ bounds = chip.getBounds();
if (getWidth() > 0 && bounds.right - bounds.left > getWidth()) {
// Need to redraw that chip.
replaceChip(chip, chip.getEntry());
@@ -853,7 +859,8 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
for (int i = 0; i < mPendingChips.size(); i++) {
String current = mPendingChips.get(i);
int tokenStart = editable.toString().indexOf(current);
- int tokenEnd = tokenStart + current.length();
+ // Always leave a space at the end between tokens.
+ int tokenEnd = tokenStart + current.length() - 1;
if (tokenStart >= 0) {
// When we have a valid token, include it with the token
// to the left.
@@ -861,7 +868,8 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
&& editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) {
tokenEnd++;
}
- createReplacementChip(tokenStart, tokenEnd, editable);
+ createReplacementChip(tokenStart, tokenEnd, editable, i < CHIP_LIMIT
+ || !mShouldShrink);
}
mPendingChipsCount--;
}
@@ -878,10 +886,10 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
} else {
// Create the "more" chip
mIndividualReplacements = new IndividualReplacementTask();
- mIndividualReplacements.execute(new ArrayList<RecipientChip>(
+ mIndividualReplacements.execute(new ArrayList<DrawableRecipientChip>(
mTemporaryRecipients.subList(0, CHIP_LIMIT)));
if (mTemporaryRecipients.size() > CHIP_LIMIT) {
- mTemporaryRecipients = new ArrayList<RecipientChip>(
+ mTemporaryRecipients = new ArrayList<DrawableRecipientChip>(
mTemporaryRecipients.subList(CHIP_LIMIT,
mTemporaryRecipients.size()));
} else {
@@ -915,17 +923,16 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
return;
}
// Find the last chip; eliminate any commit characters after it.
- RecipientChip[] chips = getSortedRecipients();
+ DrawableRecipientChip[] chips = getSortedRecipients();
+ Spannable spannable = getSpannable();
if (chips != null && chips.length > 0) {
int end;
- ImageSpan lastSpan;
mMoreChip = getMoreChip();
if (mMoreChip != null) {
- lastSpan = mMoreChip;
+ end = spannable.getSpanEnd(mMoreChip);
} else {
- lastSpan = getLastChip();
+ end = getSpannable().getSpanEnd(getLastChip());
}
- end = getSpannable().getSpanEnd(lastSpan);
Editable editable = getText();
int length = editable.length();
if (length > end) {
@@ -943,34 +950,35 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* Create a chip that represents just the email address of a recipient. At some later
* point, this chip will be attached to a real contact entry, if one exists.
*/
- private void createReplacementChip(int tokenStart, int tokenEnd, Editable editable) {
+ // VisibleForTesting
+ void createReplacementChip(int tokenStart, int tokenEnd, Editable editable,
+ boolean visible) {
if (alreadyHasChip(tokenStart, tokenEnd)) {
// There is already a chip present at this location.
// Don't recreate it.
return;
}
String token = editable.toString().substring(tokenStart, tokenEnd);
- int commitCharIndex = token.trim().lastIndexOf(COMMIT_CHAR_COMMA);
- if (commitCharIndex == token.length() - 1) {
- token = token.substring(0, token.length() - 1);
+ final String trimmedToken = token.trim();
+ int commitCharIndex = trimmedToken.lastIndexOf(COMMIT_CHAR_COMMA);
+ if (commitCharIndex != -1 && commitCharIndex == trimmedToken.length() - 1) {
+ token = trimmedToken.substring(0, trimmedToken.length() - 1);
}
RecipientEntry entry = createTokenizedEntry(token);
if (entry != null) {
- String destText = createAddressText(entry);
- SpannableString chipText = new SpannableString(destText);
- int end = getSelectionEnd();
- int start = mTokenizer != null ? mTokenizer.findTokenStart(getText(), end) : 0;
- RecipientChip chip = null;
+ DrawableRecipientChip chip = null;
try {
if (!mNoChips) {
- /* leave space for the contact icon if this is not just an email address */
- chip = constructChipSpan(
- entry,
- start,
- false,
- TextUtils.isEmpty(entry.getDisplayName())
- || TextUtils.equals(entry.getDisplayName(),
- entry.getDestination()));
+ /*
+ * 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);
}
} catch (NullPointerException e) {
Log.e(TAG, e.getMessage(), e);
@@ -979,9 +987,9 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
// Add this chip to the list of entries "to replace"
if (chip != null) {
if (mTemporaryRecipients == null) {
- mTemporaryRecipients = new ArrayList<RecipientChip>();
+ mTemporaryRecipients = new ArrayList<DrawableRecipientChip>();
}
- chip.setOriginalText(chipText.toString());
+ chip.setOriginalText(token);
mTemporaryRecipients.add(chip);
}
}
@@ -999,70 +1007,68 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
return match.matches();
}
- private RecipientEntry createTokenizedEntry(String token) {
+ // VisibleForTesting
+ RecipientEntry createTokenizedEntry(final String token) {
if (TextUtils.isEmpty(token)) {
return null;
}
if (isPhoneQuery() && isPhoneNumber(token)) {
- return RecipientEntry
- .constructFakeEntry(token);
+ return RecipientEntry.constructFakePhoneEntry(token, true);
}
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token);
String display = null;
- if (isValid(token) && tokens != null && tokens.length > 0) {
+ boolean isValid = isValid(token);
+ if (isValid && tokens != null && tokens.length > 0) {
// If we can get a name from tokenizing, then generate an entry from
// this.
display = tokens[0].getName();
if (!TextUtils.isEmpty(display)) {
- if (!isPhoneQuery()) {
- if (!TextUtils.isEmpty(token)) {
- token = token.trim();
- }
- char charAt = token.charAt(token.length() - 1);
- if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) {
- token = token.substring(0, token.length() - 1);
- }
- }
- return RecipientEntry.constructGeneratedEntry(display, token);
+ return RecipientEntry.constructGeneratedEntry(display, tokens[0].getAddress(),
+ isValid);
} else {
display = tokens[0].getAddress();
if (!TextUtils.isEmpty(display)) {
- return RecipientEntry.constructFakeEntry(display);
+ return RecipientEntry.constructFakeEntry(display, isValid);
}
}
}
// Unable to validate the token or to create a valid token from it.
// Just create a chip the user can edit.
String validatedToken = null;
- if (mValidator != null && !mValidator.isValid(token)) {
+ if (mValidator != null && !isValid) {
// Try fixing up the entry using the validator.
validatedToken = mValidator.fixText(token).toString();
if (!TextUtils.isEmpty(validatedToken)) {
if (validatedToken.contains(token)) {
- // protect against the case of a validator with a null domain,
+ // protect against the case of a validator with a null
+ // domain,
// which doesn't add a domain to the token
Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(validatedToken);
if (tokenized.length > 0) {
validatedToken = tokenized[0].getAddress();
+ isValid = true;
}
} else {
- // We ran into a case where the token was invalid and removed
- // by the validator. In this case, just use the original token
+ // We ran into a case where the token was invalid and
+ // removed
+ // by the validator. In this case, just use the original
+ // token
// and let the user sort out the error chip.
validatedToken = null;
+ isValid = false;
}
}
}
// Otherwise, fallback to just creating an editable email address chip.
- return RecipientEntry
- .constructFakeEntry(!TextUtils.isEmpty(validatedToken) ? validatedToken : token);
+ return RecipientEntry.constructFakeEntry(
+ !TextUtils.isEmpty(validatedToken) ? validatedToken : token, isValid);
}
private boolean isValid(String text) {
return mValidator == null ? true : mValidator.isValid(text);
}
- private String tokenizeAddress(String destination) {
+ private static String tokenizeAddress(String destination) {
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination);
if (tokens != null && tokens.length > 0) {
return tokens[0].getAddress();
@@ -1115,20 +1121,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
- case KeyEvent.KEYCODE_ENTER:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- if (event.hasNoModifiers()) {
- if (commitDefault()) {
- return true;
- }
- if (mSelectedChip != null) {
- clearSelectedChip();
- return true;
- } else if (focusNext()) {
- return true;
- }
- }
- break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
if (mSelectedChip != null) {
@@ -1136,9 +1128,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
} else {
commitDefault();
}
- if (focusNext()) {
- return true;
- }
}
break;
}
@@ -1246,10 +1235,10 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
return;
}
// Find the last chip.
- RecipientChip[] recips = getSortedRecipients();
+ DrawableRecipientChip[] recips = getSortedRecipients();
if (recips != null && recips.length > 0) {
- RecipientChip last = recips[recips.length - 1];
- RecipientChip beforeLast = null;
+ DrawableRecipientChip last = recips[recips.length - 1];
+ DrawableRecipientChip beforeLast = null;
if (recips.length > 1) {
beforeLast = recips[recips.length - 2];
}
@@ -1280,7 +1269,8 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if (mNoChips) {
return true;
}
- RecipientChip[] chips = getSpannable().getSpans(start, end, RecipientChip.class);
+ DrawableRecipientChip[] chips =
+ getSpannable().getSpans(start, end, DrawableRecipientChip.class);
if ((chips == null || chips.length == 0)) {
return false;
}
@@ -1299,7 +1289,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
setSelection(end);
String text = getText().toString().substring(start, end);
if (!TextUtils.isEmpty(text)) {
- RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
+ RecipientEntry entry = RecipientEntry.constructFakeEntry(text, isValid(text));
QwertyKeyListener.markAsReplaced(editable, start, end, "");
CharSequence chipText = createChip(entry, false);
int selEnd = getSelectionEnd();
@@ -1323,8 +1313,21 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
removeChip(mSelectedChip);
}
- if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
- return true;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.hasNoModifiers()) {
+ if (commitDefault()) {
+ return true;
+ }
+ if (mSelectedChip != null) {
+ clearSelectedChip();
+ return true;
+ } else if (focusNext()) {
+ return true;
+ }
+ }
+ break;
}
return super.onKeyDown(keyCode, event);
@@ -1335,11 +1338,11 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
return getText();
}
- private int getChipStart(RecipientChip chip) {
+ private int getChipStart(DrawableRecipientChip chip) {
return getSpannable().getSpanStart(chip);
}
- private int getChipEnd(RecipientChip chip) {
+ private int getChipEnd(DrawableRecipientChip chip) {
return getSpannable().getSpanEnd(chip);
}
@@ -1359,7 +1362,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
// If this is a RecipientChip, don't filter
// on its contents.
Spannable span = getSpannable();
- RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
+ DrawableRecipientChip[] chips = span.getSpans(start, end, DrawableRecipientChip.class);
if (chips != null && chips.length > 0) {
return;
}
@@ -1417,7 +1420,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
float x = event.getX();
float y = event.getY();
int offset = putOffsetInRange(getOffsetForPosition(x, y));
- RecipientChip currentChip = findChip(offset);
+ DrawableRecipientChip currentChip = findChip(offset);
if (currentChip != null) {
if (action == MotionEvent.ACTION_UP) {
if (mSelectedChip != null && mSelectedChip != currentChip) {
@@ -1449,44 +1452,54 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
}
- private void showAlternates(RecipientChip currentChip, ListPopupWindow alternatesPopup,
- int width, Context context) {
- int line = getLayout().getLineForOffset(getChipStart(currentChip));
- int bottom;
- if (line == getLineCount() -1) {
- bottom = 0;
- } else {
- bottom = -(int) ((mChipHeight + (2 * mLineSpacingExtra)) * (Math.abs(getLineCount() - 1
- - line)));
- }
- // Align the alternates popup with the left side of the View,
- // regardless of the position of the chip tapped.
- alternatesPopup.setWidth(width);
- alternatesPopup.setAnchorView(this);
- alternatesPopup.setVerticalOffset(bottom);
- alternatesPopup.setAdapter(createAlternatesAdapter(currentChip));
- alternatesPopup.setOnItemClickListener(mAlternatesListener);
- // Clear the checked item.
- mCheckedItem = -1;
- alternatesPopup.show();
- ListView listView = alternatesPopup.getListView();
- listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
- // Checked item would be -1 if the adapter has not
- // loaded the view that should be checked yet. The
- // variable will be set correctly when onCheckedItemChanged
- // is called in a separate thread.
- if (mCheckedItem != -1) {
- listView.setItemChecked(mCheckedItem, true);
- mCheckedItem = -1;
- }
+ private void showAlternates(final DrawableRecipientChip currentChip,
+ final ListPopupWindow alternatesPopup, final int width) {
+ new AsyncTask<Void, Void, ListAdapter>() {
+ @Override
+ protected ListAdapter doInBackground(final Void... params) {
+ return createAlternatesAdapter(currentChip);
+ }
+
+ @Override
+ protected void onPostExecute(final ListAdapter result) {
+ int line = getLayout().getLineForOffset(getChipStart(currentChip));
+ int bottom;
+ if (line == getLineCount() -1) {
+ bottom = 0;
+ } else {
+ bottom = -(int) ((mChipHeight + (2 * mLineSpacingExtra)) * (Math
+ .abs(getLineCount() - 1 - line)));
+ }
+ // Align the alternates popup with the left side of the View,
+ // regardless of the position of the chip tapped.
+ alternatesPopup.setWidth(width);
+ alternatesPopup.setAnchorView(RecipientEditTextView.this);
+ alternatesPopup.setVerticalOffset(bottom);
+ alternatesPopup.setAdapter(result);
+ alternatesPopup.setOnItemClickListener(mAlternatesListener);
+ // Clear the checked item.
+ mCheckedItem = -1;
+ alternatesPopup.show();
+ ListView listView = alternatesPopup.getListView();
+ listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ // Checked item would be -1 if the adapter has not
+ // loaded the view that should be checked yet. The
+ // variable will be set correctly when onCheckedItemChanged
+ // is called in a separate thread.
+ if (mCheckedItem != -1) {
+ listView.setItemChecked(mCheckedItem, true);
+ mCheckedItem = -1;
+ }
+ }
+ }.execute((Void[]) null);
}
- private ListAdapter createAlternatesAdapter(RecipientChip chip) {
+ private ListAdapter createAlternatesAdapter(DrawableRecipientChip chip) {
return new RecipientAlternatesAdapter(getContext(), chip.getContactId(), chip.getDataId(),
- mAlternatesLayout, ((BaseRecipientAdapter)getAdapter()).getQueryType(), this);
+ ((BaseRecipientAdapter)getAdapter()).getQueryType(), this);
}
- private ListAdapter createSingleAddressAdapter(RecipientChip currentChip) {
+ private ListAdapter createSingleAddressAdapter(DrawableRecipientChip currentChip) {
return new SingleRecipientArrayAdapter(getContext(), mAlternatesLayout, currentChip
.getEntry());
}
@@ -1530,18 +1543,19 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
return offset;
}
- private int findText(Editable text, int offset) {
+ private static int findText(Editable text, int offset) {
if (text.charAt(offset) != ' ') {
return offset;
}
return -1;
}
- private RecipientChip findChip(int offset) {
- RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class);
+ private DrawableRecipientChip findChip(int offset) {
+ DrawableRecipientChip[] chips =
+ getSpannable().getSpans(0, getText().length(), DrawableRecipientChip.class);
// Find the chip that contains this offset.
for (int i = 0; i < chips.length; i++) {
- RecipientChip chip = chips[i];
+ DrawableRecipientChip chip = chips[i];
int start = getChipStart(chip);
int end = getChipEnd(chip);
if (offset >= start && offset <= end) {
@@ -1588,14 +1602,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
display = null;
}
- if (address != null && !(isPhoneQuery() && isPhoneNumber(address))) {
- // Tokenize out the address in case the address already
- // contained the username as well.
- Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(address);
- if (tokenized != null && tokenized.length > 0) {
- address = tokenized[0].getAddress();
- }
- }
if (!TextUtils.isEmpty(display)) {
return display;
} else if (!TextUtils.isEmpty(address)){
@@ -1612,13 +1618,11 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
SpannableString chipText = null;
// Always leave a blank space at the end of a chip.
- int end = getSelectionEnd();
- int start = mTokenizer.findTokenStart(getText(), end);
- int textLength = displayText.length()-1;
+ int textLength = displayText.length() - 1;
chipText = new SpannableString(displayText);
if (!mNoChips) {
try {
- RecipientChip chip = constructChipSpan(entry, start, pressed,
+ DrawableRecipientChip chip = constructChipSpan(entry, pressed,
false /* leave space for contact icon */);
chipText.setSpan(chip, 0, textLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -1637,6 +1641,9 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
*/
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (position < 0) {
+ return;
+ }
submitItemAtPosition(position);
}
@@ -1671,12 +1678,12 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
String destination = item.getDestination();
if (!isPhoneQuery() && item.getContactId() == RecipientEntry.GENERATED_CONTACT) {
entry = RecipientEntry.constructGeneratedEntry(item.getDisplayName(),
- destination);
+ destination, item.isValid());
} else if (RecipientEntry.isCreatedRecipient(item.getContactId())
&& (TextUtils.isEmpty(item.getDisplayName())
|| TextUtils.equals(item.getDisplayName(), destination)
|| (mValidator != null && !mValidator.isValid(destination)))) {
- entry = RecipientEntry.constructFakeEntry(destination);
+ entry = RecipientEntry.constructFakeEntry(destination, item.isValid());
} else {
entry = item;
}
@@ -1686,9 +1693,9 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
/** Returns a collection of contact Id for each chip inside this View. */
/* package */ Collection<Long> getContactIds() {
final Set<Long> result = new HashSet<Long>();
- RecipientChip[] chips = getSortedRecipients();
+ DrawableRecipientChip[] chips = getSortedRecipients();
if (chips != null) {
- for (RecipientChip chip : chips) {
+ for (DrawableRecipientChip chip : chips) {
result.add(chip.getContactId());
}
}
@@ -1699,9 +1706,9 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
/** 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>();
- RecipientChip [] chips = getSortedRecipients();
+ DrawableRecipientChip [] chips = getSortedRecipients();
if (chips != null) {
- for (RecipientChip chip : chips) {
+ for (DrawableRecipientChip chip : chips) {
result.add(chip.getDataId());
}
}
@@ -1709,16 +1716,16 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
// Visible for testing.
- /* package */RecipientChip[] getSortedRecipients() {
- RecipientChip[] recips = getSpannable()
- .getSpans(0, getText().length(), RecipientChip.class);
- ArrayList<RecipientChip> recipientsList = new ArrayList<RecipientChip>(Arrays
- .asList(recips));
+ /* package */DrawableRecipientChip[] getSortedRecipients() {
+ DrawableRecipientChip[] recips = getSpannable()
+ .getSpans(0, getText().length(), DrawableRecipientChip.class);
+ ArrayList<DrawableRecipientChip> recipientsList = new ArrayList<DrawableRecipientChip>(
+ Arrays.asList(recips));
final Spannable spannable = getSpannable();
- Collections.sort(recipientsList, new Comparator<RecipientChip>() {
+ Collections.sort(recipientsList, new Comparator<DrawableRecipientChip>() {
@Override
- public int compare(RecipientChip first, RecipientChip second) {
+ public int compare(DrawableRecipientChip first, DrawableRecipientChip second) {
int firstStart = spannable.getSpanStart(first);
int secondStart = spannable.getSpanStart(second);
if (firstStart < secondStart) {
@@ -1730,7 +1737,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
}
});
- return recipientsList.toArray(new RecipientChip[recipientsList.size()]);
+ return recipientsList.toArray(new DrawableRecipientChip[recipientsList.size()]);
}
@Override
@@ -1837,7 +1844,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if (tempMore.length > 0) {
getSpannable().removeSpan(tempMore[0]);
}
- RecipientChip[] recipients = getSortedRecipients();
+ DrawableRecipientChip[] recipients = getSortedRecipients();
if (recipients == null || recipients.length <= CHIP_LIMIT) {
mMoreChip = null;
@@ -1847,7 +1854,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
int numRecipients = recipients.length;
int overage = numRecipients - CHIP_LIMIT;
MoreImageSpan moreSpan = createMoreSpan(overage);
- mRemovedSpans = new ArrayList<RecipientChip>();
+ mRemovedSpans = new ArrayList<DrawableRecipientChip>();
int totalReplaceStart = 0;
int totalReplaceEnd = 0;
Editable text = getText();
@@ -1894,7 +1901,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
// Re-add the spans that were removed.
if (mRemovedSpans != null && mRemovedSpans.size() > 0) {
// Recreate each removed span.
- RecipientChip[] recipients = getSortedRecipients();
+ DrawableRecipientChip[] recipients = getSortedRecipients();
// Start the search for tokens after the last currently visible
// chip.
if (recipients == null || recipients.length == 0) {
@@ -1902,7 +1909,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
int end = span.getSpanEnd(recipients[recipients.length - 1]);
Editable editable = getText();
- for (RecipientChip chip : mRemovedSpans) {
+ for (DrawableRecipientChip chip : mRemovedSpans) {
int chipStart;
int chipEnd;
String token;
@@ -1937,7 +1944,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* @return A RecipientChip in the selected state or null if the chip
* just contained an email address.
*/
- private RecipientChip selectChip(RecipientChip currentChip) {
+ private DrawableRecipientChip selectChip(DrawableRecipientChip currentChip) {
if (shouldShowEditableText(currentChip)) {
CharSequence text = currentChip.getValue();
Editable editable = getText();
@@ -1950,18 +1957,18 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
setSelection(editable.length());
editable.append(text);
return constructChipSpan(
- RecipientEntry.constructFakeEntry((String) text),
- getSelectionStart(), true, false);
+ 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);
- RecipientChip newChip;
+ DrawableRecipientChip newChip;
try {
if (mNoChips) {
return null;
}
- newChip = constructChipSpan(currentChip.getEntry(), start, true, false);
+ newChip = constructChipSpan(currentChip.getEntry(), true, false);
} catch (NullPointerException e) {
Log.e(TAG, e.getMessage(), e);
return null;
@@ -1977,16 +1984,16 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if (shouldShowEditableText(newChip)) {
scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
}
- showAddress(newChip, mAddressPopup, getWidth(), getContext());
+ showAddress(newChip, mAddressPopup, getWidth());
setCursorVisible(false);
return newChip;
} else {
int start = getChipStart(currentChip);
int end = getChipEnd(currentChip);
getSpannable().removeSpan(currentChip);
- RecipientChip newChip;
+ DrawableRecipientChip newChip;
try {
- newChip = constructChipSpan(currentChip.getEntry(), start, true, false);
+ newChip = constructChipSpan(currentChip.getEntry(), true, false);
} catch (NullPointerException e) {
Log.e(TAG, e.getMessage(), e);
return null;
@@ -2002,20 +2009,20 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if (shouldShowEditableText(newChip)) {
scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
}
- showAlternates(newChip, mAlternatesPopup, getWidth(), getContext());
+ showAlternates(newChip, mAlternatesPopup, getWidth());
setCursorVisible(false);
return newChip;
}
}
- private boolean shouldShowEditableText(RecipientChip currentChip) {
+ private boolean shouldShowEditableText(DrawableRecipientChip currentChip) {
long contactId = currentChip.getContactId();
return contactId == RecipientEntry.INVALID_CONTACT
|| (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
}
- private void showAddress(final RecipientChip currentChip, final ListPopupWindow popup,
- int width, Context context) {
+ private void showAddress(final DrawableRecipientChip currentChip, final ListPopupWindow popup,
+ int width) {
int line = getLayout().getLineForOffset(getChipStart(currentChip));
int bottom = calculateOffsetFromBottom(line);
// Align the alternates popup with the left side of the View,
@@ -2042,7 +2049,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* the chip without a delete icon and with an unfocused background. This is
* called when the RecipientChip no longer has focus.
*/
- private void unselectChip(RecipientChip chip) {
+ private void unselectChip(DrawableRecipientChip chip) {
int start = getChipStart(chip);
int end = getChipEnd(chip);
Editable editable = getText();
@@ -2057,7 +2064,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
editable.removeSpan(chip);
try {
if (!mNoChips) {
- editable.setSpan(constructChipSpan(chip.getEntry(), start, false, false),
+ editable.setSpan(constructChipSpan(chip.getEntry(), false, false),
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} catch (NullPointerException e) {
@@ -2080,9 +2087,10 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* right after the selected chip.
* @return boolean
*/
- private boolean isInDelete(RecipientChip chip, int offset, float x, float y) {
+ private boolean isInDelete(DrawableRecipientChip chip, int offset, float x, float y) {
// Figure out the bounds of this chip and whether or not
// the user clicked in the X portion.
+ // TODO: Should x and y be used, or removed?
return chip.isSelected() && offset == getChipEnd(chip);
}
@@ -2090,7 +2098,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* Remove the chip and any text associated with it from the RecipientEditTextView.
*/
// Visible for testing.
- /*pacakge*/ void removeChip(RecipientChip chip) {
+ /*pacakge*/ void removeChip(DrawableRecipientChip chip) {
Spannable spannable = getSpannable();
int spanStart = spannable.getSpanStart(chip);
int spanEnd = spannable.getSpanEnd(chip);
@@ -2119,7 +2127,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* that uses the contact data provided.
*/
// Visible for testing.
- /*package*/ void replaceChip(RecipientChip chip, RecipientEntry entry) {
+ /*package*/ void replaceChip(DrawableRecipientChip chip, RecipientEntry entry) {
boolean wasSelected = chip == mSelectedChip;
if (wasSelected) {
mSelectedChip = null;
@@ -2157,7 +2165,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* event, see if that event was in the delete icon. If so, delete it.
* Otherwise, unselect the chip.
*/
- public void onClick(RecipientChip chip, int offset, float x, float y) {
+ public void onClick(DrawableRecipientChip chip, int offset, float x, float y) {
if (chip.isSelected()) {
if (isInDelete(chip, offset, x, y)) {
removeChip(chip);
@@ -2186,9 +2194,9 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
if (TextUtils.isEmpty(s)) {
// Remove all the chips spans.
Spannable spannable = getSpannable();
- RecipientChip[] chips = spannable.getSpans(0, getText().length(),
- RecipientChip.class);
- for (RecipientChip chip : chips) {
+ DrawableRecipientChip[] chips = spannable.getSpans(0, getText().length(),
+ DrawableRecipientChip.class);
+ for (DrawableRecipientChip chip : chips) {
spannable.removeSpan(chip);
}
if (mMoreChip != null) {
@@ -2202,16 +2210,23 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
return;
}
// If the user is editing a chip, don't clear it.
- if (mSelectedChip != null
- && shouldShowEditableText(mSelectedChip)) {
- setCursorVisible(true);
- setSelection(getText().length());
- clearSelectedChip();
+ if (mSelectedChip != null) {
+ if (!isGeneratedContact(mSelectedChip)) {
+ setCursorVisible(true);
+ setSelection(getText().length());
+ clearSelectedChip();
+ } else {
+ return;
+ }
}
int length = s.length();
// Make sure there is content there to parse and that it is
// not just the commit character.
if (length > 1) {
+ if (lastCharacterIsCommitCharacter(s)) {
+ commitByCharacter();
+ return;
+ }
char last;
int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
int len = length() - 1;
@@ -2220,9 +2235,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
} else {
last = s.charAt(len);
}
- if (last == COMMIT_CHAR_SEMICOLON || last == COMMIT_CHAR_COMMA) {
- commitByCharacter();
- } else if (last == COMMIT_CHAR_SPACE) {
+ if (last == COMMIT_CHAR_SPACE) {
if (!isPhoneQuery()) {
// Check if this is a valid email address. If it is,
// commit it.
@@ -2241,14 +2254,15 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
- // This is a delete; check to see if the insertion point is on a space
+ // The user deleted some text OR some text was replaced; check to
+ // see if the insertion point is on a space
// following a chip.
if (before - count == 1) {
// If the item deleted is a space, and the thing before the
// space is a chip, delete the entire span.
int selStart = getSelectionStart();
- RecipientChip[] repl = getSpannable().getSpans(selStart, selStart,
- RecipientChip.class);
+ DrawableRecipientChip[] repl = getSpannable().getSpans(selStart, selStart,
+ DrawableRecipientChip.class);
if (repl.length > 0) {
// There is a chip there! Just remove it.
Editable editable = getText();
@@ -2262,6 +2276,14 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
editable.delete(tokenStart, tokenEnd);
getSpannable().removeSpan(repl[0]);
}
+ } else if (count > before) {
+ if (mSelectedChip != null
+ && isGeneratedContact(mSelectedChip)) {
+ if (lastCharacterIsCommitCharacter(s)) {
+ commitByCharacter();
+ return;
+ }
+ }
}
}
@@ -2271,6 +2293,24 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
}
+ public boolean lastCharacterIsCommitCharacter(CharSequence s) {
+ char last;
+ int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
+ int len = length() - 1;
+ if (end != len) {
+ last = s.charAt(end);
+ } else {
+ last = s.charAt(len);
+ }
+ return last == COMMIT_CHAR_COMMA || last == COMMIT_CHAR_SEMICOLON;
+ }
+
+ public boolean isGeneratedContact(DrawableRecipientChip chip) {
+ long contactId = chip.getContactId();
+ return contactId == RecipientEntry.INVALID_CONTACT
+ || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
+ }
+
/**
* Handles pasting a {@link ClipData} to this {@link RecipientEditTextView}.
*/
@@ -2309,7 +2349,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
private void handlePasteAndReplace() {
- ArrayList<RecipientChip> created = handlePaste();
+ ArrayList<DrawableRecipientChip> created = handlePaste();
if (created != null && created.size() > 0) {
// Perform reverse lookups on the pasted contacts.
IndividualReplacementTask replace = new IndividualReplacementTask();
@@ -2318,27 +2358,30 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
// Visible for testing.
- /* package */ArrayList<RecipientChip> handlePaste() {
+ /* package */ArrayList<DrawableRecipientChip> handlePaste() {
String text = getText().toString();
int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
String lastAddress = text.substring(originalTokenStart);
int tokenStart = originalTokenStart;
- int prevTokenStart = tokenStart;
- RecipientChip findChip = null;
- ArrayList<RecipientChip> created = new ArrayList<RecipientChip>();
+ int prevTokenStart = 0;
+ DrawableRecipientChip findChip = null;
+ ArrayList<DrawableRecipientChip> created = new ArrayList<DrawableRecipientChip>();
if (tokenStart != 0) {
// There are things before this!
- while (tokenStart != 0 && findChip == null) {
+ while (tokenStart != 0 && findChip == null && tokenStart != prevTokenStart) {
prevTokenStart = tokenStart;
tokenStart = mTokenizer.findTokenStart(text, tokenStart);
findChip = findChip(tokenStart);
+ if (tokenStart == originalTokenStart && findChip == null) {
+ break;
+ }
}
if (tokenStart != originalTokenStart) {
if (findChip != null) {
tokenStart = prevTokenStart;
}
int tokenEnd;
- RecipientChip createdChip;
+ DrawableRecipientChip createdChip;
while (tokenStart < originalTokenStart) {
tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(getText().toString(),
tokenStart));
@@ -2382,12 +2425,12 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> {
- private RecipientChip createFreeChip(RecipientEntry entry) {
+ private DrawableRecipientChip createFreeChip(RecipientEntry entry) {
try {
if (mNoChips) {
return null;
}
- return constructChipSpan(entry, -1, false,
+ return constructChipSpan(entry, false,
false /*leave space for contact icon */);
} catch (NullPointerException e) {
Log.e(TAG, e.getMessage(), e);
@@ -2396,6 +2439,35 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
}
@Override
+ protected void onPreExecute() {
+ // Ensure everything is in chip-form already, so we don't have text that slowly gets
+ // replaced
+ final List<DrawableRecipientChip> originalRecipients =
+ new ArrayList<DrawableRecipientChip>();
+ final DrawableRecipientChip[] existingChips = getSortedRecipients();
+ for (int i = 0; i < existingChips.length; i++) {
+ originalRecipients.add(existingChips[i]);
+ }
+ if (mRemovedSpans != null) {
+ originalRecipients.addAll(mRemovedSpans);
+ }
+
+ final List<DrawableRecipientChip> replacements =
+ new ArrayList<DrawableRecipientChip>(originalRecipients.size());
+
+ for (final DrawableRecipientChip chip : originalRecipients) {
+ if (RecipientEntry.isCreatedRecipient(chip.getEntry().getContactId())
+ && getSpannable().getSpanStart(chip) != -1) {
+ replacements.add(createFreeChip(chip.getEntry()));
+ } else {
+ replacements.add(null);
+ }
+ }
+
+ processReplacements(originalRecipients, replacements);
+ }
+
+ @Override
protected Void doInBackground(Void... params) {
if (mIndividualReplacements != null) {
mIndividualReplacements.cancel(true);
@@ -2403,115 +2475,192 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
// For each chip in the list, look up the matching contact.
// If there is a match, replace that chip with the matching
// chip.
- final ArrayList<RecipientChip> originalRecipients = new ArrayList<RecipientChip>();
- RecipientChip[] existingChips = getSortedRecipients();
+ final ArrayList<DrawableRecipientChip> recipients =
+ new ArrayList<DrawableRecipientChip>();
+ DrawableRecipientChip[] existingChips = getSortedRecipients();
for (int i = 0; i < existingChips.length; i++) {
- originalRecipients.add(existingChips[i]);
+ recipients.add(existingChips[i]);
}
if (mRemovedSpans != null) {
- originalRecipients.addAll(mRemovedSpans);
+ recipients.addAll(mRemovedSpans);
}
ArrayList<String> addresses = new ArrayList<String>();
- RecipientChip chip;
- for (int i = 0; i < originalRecipients.size(); i++) {
- chip = originalRecipients.get(i);
+ DrawableRecipientChip chip;
+ for (int i = 0; i < recipients.size(); i++) {
+ chip = recipients.get(i);
if (chip != null) {
addresses.add(createAddressText(chip.getEntry()));
}
}
- HashMap<String, RecipientEntry> entries = RecipientAlternatesAdapter
- .getMatchingRecipients(getContext(), addresses);
- final ArrayList<RecipientChip> replacements = new ArrayList<RecipientChip>();
- for (final RecipientChip temp : originalRecipients) {
- RecipientEntry entry = null;
- if (RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())
- && getSpannable().getSpanStart(temp) != -1) {
- // Replace this.
- entry = createValidatedEntry(entries.get(tokenizeAddress(temp.getEntry()
- .getDestination())));
- }
- if (entry != null) {
- replacements.add(createFreeChip(entry));
- } else {
- replacements.add(temp);
- }
+ final BaseRecipientAdapter adapter = (BaseRecipientAdapter) getAdapter();
+ if (adapter == null) {
+ return null;
}
+ RecipientAlternatesAdapter.getMatchingRecipients(getContext(), addresses,
+ adapter.getAccount(), new RecipientMatchCallback() {
+ @Override
+ public void matchesFound(Map<String, RecipientEntry> entries) {
+ final ArrayList<DrawableRecipientChip> replacements =
+ new ArrayList<DrawableRecipientChip>();
+ for (final DrawableRecipientChip temp : recipients) {
+ RecipientEntry entry = null;
+ if (temp != null && RecipientEntry.isCreatedRecipient(
+ temp.getEntry().getContactId())
+ && getSpannable().getSpanStart(temp) != -1) {
+ // Replace this.
+ entry = createValidatedEntry(
+ entries.get(tokenizeAddress(temp.getEntry()
+ .getDestination())));
+ }
+ if (entry != null) {
+ replacements.add(createFreeChip(entry));
+ } else {
+ replacements.add(null);
+ }
+ }
+ processReplacements(recipients, replacements);
+ }
+
+ @Override
+ public void matchesNotFound(final Set<String> unfoundAddresses) {
+ final List<DrawableRecipientChip> replacements =
+ new ArrayList<DrawableRecipientChip>(unfoundAddresses.size());
+
+ for (final DrawableRecipientChip temp : recipients) {
+ if (temp != null && RecipientEntry.isCreatedRecipient(
+ temp.getEntry().getContactId())
+ && getSpannable().getSpanStart(temp) != -1) {
+ if (unfoundAddresses.contains(
+ temp.getEntry().getDestination())) {
+ replacements.add(createFreeChip(temp.getEntry()));
+ } else {
+ replacements.add(null);
+ }
+ } else {
+ replacements.add(null);
+ }
+ }
+
+ processReplacements(recipients, replacements);
+ }
+ });
+ return null;
+ }
+
+ private void processReplacements(final List<DrawableRecipientChip> recipients,
+ final List<DrawableRecipientChip> replacements) {
if (replacements != null && replacements.size() > 0) {
- mHandler.post(new Runnable() {
+ final Runnable runnable = new Runnable() {
@Override
public void run() {
- Editable oldText = getText();
- int start, end;
+ final Editable text = new SpannableStringBuilder(getText());
int i = 0;
- for (RecipientChip chip : originalRecipients) {
- // Find the location of the chip in the text currently shown.
- start = oldText.getSpanStart(chip);
- if (start != -1) {
- end = oldText.getSpanEnd(chip);
- oldText.removeSpan(chip);
- RecipientChip replacement = replacements.get(i);
- // Make sure we always have just 1 space at the
- // end to separate this chip from the next chip.
- SpannableString displayText = new SpannableString(
- createAddressText(replacement.getEntry()).trim() + " ");
- displayText.setSpan(replacement, 0, displayText.length()-1,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- // Replace the old text we found with with the new display text,
- // which now may also contain the display name of the recipient.
- oldText.replace(start, end, displayText);
- replacement.setOriginalText(displayText.toString());
+ for (final DrawableRecipientChip chip : recipients) {
+ final DrawableRecipientChip replacement = replacements.get(i);
+ if (replacement != null) {
+ final RecipientEntry oldEntry = chip.getEntry();
+ final RecipientEntry newEntry = replacement.getEntry();
+ final boolean isBetter =
+ RecipientAlternatesAdapter.getBetterRecipient(
+ oldEntry, newEntry) == newEntry;
+
+ if (isBetter) {
+ // Find the location of the chip in the text currently shown.
+ final int start = text.getSpanStart(chip);
+ if (start != -1) {
+ // Replacing the entirety of what the chip represented,
+ // including the extra space dividing it from other chips.
+ final int end =
+ Math.min(text.getSpanEnd(chip) + 1, text.length());
+ text.removeSpan(chip);
+ // Make sure we always have just 1 space at the end to
+ // separate this chip from the next chip.
+ final SpannableString displayText =
+ new SpannableString(createAddressText(
+ replacement.getEntry()).trim() + " ");
+ displayText.setSpan(replacement, 0,
+ displayText.length() - 1,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ // Replace the old text we found with with the new display
+ // text, which now may also contain the display name of the
+ // recipient.
+ text.replace(start, end, displayText);
+ replacement.setOriginalText(displayText.toString());
+ replacements.set(i, null);
+
+ recipients.set(i, replacement);
+ }
+ }
}
i++;
}
- originalRecipients.clear();
+ setText(text);
}
- });
+ };
+
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ runnable.run();
+ } else {
+ mHandler.post(runnable);
+ }
}
- return null;
}
}
- private class IndividualReplacementTask extends AsyncTask<Object, Void, Void> {
- @SuppressWarnings("unchecked")
+ private class IndividualReplacementTask
+ extends AsyncTask<ArrayList<DrawableRecipientChip>, Void, Void> {
@Override
- protected Void doInBackground(Object... params) {
+ protected Void doInBackground(ArrayList<DrawableRecipientChip>... params) {
// For each chip in the list, look up the matching contact.
// If there is a match, replace that chip with the matching
// chip.
- final ArrayList<RecipientChip> originalRecipients =
- (ArrayList<RecipientChip>) params[0];
+ final ArrayList<DrawableRecipientChip> originalRecipients = params[0];
ArrayList<String> addresses = new ArrayList<String>();
- RecipientChip chip;
+ DrawableRecipientChip chip;
for (int i = 0; i < originalRecipients.size(); i++) {
chip = originalRecipients.get(i);
if (chip != null) {
addresses.add(createAddressText(chip.getEntry()));
}
}
- HashMap<String, RecipientEntry> entries = RecipientAlternatesAdapter
- .getMatchingRecipients(getContext(), addresses);
- for (final RecipientChip temp : originalRecipients) {
- if (RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())
- && getSpannable().getSpanStart(temp) != -1) {
- // Replace this.
- RecipientEntry entry = createValidatedEntry(entries.get(tokenizeAddress(
- temp.getEntry().getDestination()).toLowerCase()));
- // If we don't have a validated contact match, just use the
- // entry as it existed before.
- if (entry == null && !isPhoneQuery()) {
- entry = temp.getEntry();
- }
- final RecipientEntry tempEntry = entry;
- if (tempEntry != null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- replaceChip(temp, tempEntry);
+ RecipientAlternatesAdapter.getMatchingRecipients(getContext(), addresses,
+ ((BaseRecipientAdapter) getAdapter()).getAccount(),
+ new RecipientMatchCallback() {
+
+ @Override
+ public void matchesFound(Map<String, RecipientEntry> entries) {
+ for (final DrawableRecipientChip temp : originalRecipients) {
+ if (RecipientEntry.isCreatedRecipient(temp.getEntry()
+ .getContactId())
+ && getSpannable().getSpanStart(temp) != -1) {
+ // Replace this.
+ RecipientEntry entry = createValidatedEntry(entries
+ .get(tokenizeAddress(temp.getEntry().getDestination())
+ .toLowerCase()));
+ // If we don't have a validated contact
+ // match, just use the
+ // entry as it existed before.
+ if (entry == null && !isPhoneQuery()) {
+ entry = temp.getEntry();
+ }
+ final RecipientEntry tempEntry = entry;
+ if (tempEntry != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ replaceChip(temp, tempEntry);
+ }
+ });
+ }
+ }
}
- });
- }
- }
- }
+ }
+
+ @Override
+ public void matchesNotFound(final Set<String> unfoundAddresses) {
+ // No action required
+ }
+ });
return null;
}
}
@@ -2546,7 +2695,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
float x = event.getX();
float y = event.getY();
int offset = putOffsetInRange(getOffsetForPosition(x, y));
- RecipientChip currentChip = findChip(offset);
+ DrawableRecipientChip currentChip = findChip(offset);
if (currentChip != null) {
if (mDragEnabled) {
// Start drag-and-drop for the selected chip.
@@ -2568,7 +2717,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
/**
* Starts drag-and-drop for the selected chip.
*/
- private void startDrag(RecipientChip currentChip) {
+ private void startDrag(DrawableRecipientChip currentChip) {
String address = currentChip.getEntry().getDestination();
ClipData data = ClipData.newPlainText(address, address + COMMIT_CHAR_COMMA);
@@ -2603,22 +2752,22 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements
* Drag shadow for a {@link RecipientChip}.
*/
private final class RecipientChipShadow extends DragShadowBuilder {
- private final RecipientChip mChip;
+ private final DrawableRecipientChip mChip;
- public RecipientChipShadow(RecipientChip chip) {
+ public RecipientChipShadow(DrawableRecipientChip chip) {
mChip = chip;
}
@Override
public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
- Rect rect = mChip.getDrawable().getBounds();
+ Rect rect = mChip.getBounds();
shadowSize.set(rect.width(), rect.height());
shadowTouchPoint.set(rect.centerX(), rect.centerY());
}
@Override
public void onDrawShadow(Canvas canvas) {
- mChip.getDrawable().draw(canvas);
+ mChip.draw(canvas);
}
}
diff --git a/chips/src/com/android/ex/chips/RecipientEntry.java b/chips/src/com/android/ex/chips/RecipientEntry.java
index 0448229..44bc767 100644
--- a/chips/src/com/android/ex/chips/RecipientEntry.java
+++ b/chips/src/com/android/ex/chips/RecipientEntry.java
@@ -19,6 +19,8 @@ package com.android.ex.chips;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.DisplayNameSources;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
/**
* Represents one entry inside recipient auto-complete list.
@@ -65,29 +67,16 @@ public class RecipientEntry {
private final Uri mPhotoThumbnailUri;
+ private boolean mIsValid;
/**
* This can be updated after this object being constructed, when the photo is fetched
* from remote directories.
*/
private byte[] mPhotoBytes;
- private RecipientEntry(int entryType) {
- mEntryType = entryType;
- mDisplayName = null;
- mDestination = null;
- mDestinationType = INVALID_DESTINATION_TYPE;
- mDestinationLabel = null;
- mContactId = -1;
- mDataId = -1;
- mPhotoThumbnailUri = null;
- mPhotoBytes = null;
- mIsDivider = true;
- }
-
- private RecipientEntry(
- int entryType, String displayName,
- String destination, int destinationType, String destinationLabel,
- long contactId, long dataId, Uri photoThumbnailUri, boolean isFirstLevel) {
+ private RecipientEntry(int entryType, String displayName, String destination,
+ int destinationType, String destinationLabel, long contactId, long dataId,
+ Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid) {
mEntryType = entryType;
mIsFirstLevel = isFirstLevel;
mDisplayName = displayName;
@@ -99,6 +88,11 @@ public class RecipientEntry {
mPhotoThumbnailUri = photoThumbnailUri;
mPhotoBytes = null;
mIsDivider = false;
+ mIsValid = isValid;
+ }
+
+ public boolean isValid() {
+ return mIsValid;
}
/**
@@ -114,10 +108,23 @@ public class RecipientEntry {
* This address has not been resolved to a contact and therefore does not
* have a contact id or photo.
*/
- public static RecipientEntry constructFakeEntry(String address) {
- return new RecipientEntry(ENTRY_TYPE_PERSON, address, address,
+ public static RecipientEntry constructFakeEntry(final String address, final boolean isValid) {
+ final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
+ final String tokenizedAddress = tokens.length > 0 ? tokens[0].getAddress() : address;
+
+ return new RecipientEntry(ENTRY_TYPE_PERSON, tokenizedAddress, tokenizedAddress,
+ INVALID_DESTINATION_TYPE, null,
+ INVALID_CONTACT, INVALID_CONTACT, null, true, isValid);
+ }
+
+ /**
+ * Construct a RecipientEntry from just a phone number.
+ */
+ public static RecipientEntry constructFakePhoneEntry(final String phoneNumber,
+ final boolean isValid) {
+ return new RecipientEntry(ENTRY_TYPE_PERSON, phoneNumber, phoneNumber,
INVALID_DESTINATION_TYPE, null,
- INVALID_CONTACT, INVALID_CONTACT, null, true);
+ INVALID_CONTACT, INVALID_CONTACT, null, true, isValid);
}
/**
@@ -136,41 +143,37 @@ public class RecipientEntry {
* with both an associated display name. This address has not been resolved
* to a contact and therefore does not have a contact id or photo.
*/
- public static RecipientEntry constructGeneratedEntry(String display, String address) {
- return new RecipientEntry(ENTRY_TYPE_PERSON, display,
- address, INVALID_DESTINATION_TYPE, null,
- GENERATED_CONTACT, GENERATED_CONTACT, null, true);
- }
-
- public static RecipientEntry constructTopLevelEntry(
- String displayName, int displayNameSource, String destination, int destinationType,
- String destinationLabel, long contactId, long dataId, Uri photoThumbnailUri) {
- return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource, displayName,
- destination),
- destination, destinationType, destinationLabel,
- contactId, dataId,
- photoThumbnailUri, true);
- }
-
- public static RecipientEntry constructTopLevelEntry(
- String displayName, int displayNameSource, String destination, int destinationType,
- String destinationLabel, long contactId, long dataId,
- String thumbnailUriAsString) {
- return new RecipientEntry(
- ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource, displayName, destination),
- destination, destinationType, destinationLabel,
- contactId, dataId,
- (thumbnailUriAsString != null ? Uri.parse(thumbnailUriAsString) : null), true);
- }
-
- public static RecipientEntry constructSecondLevelEntry(
- String displayName, int displayNameSource, String destination, int destinationType,
- String destinationLabel, long contactId, long dataId, String thumbnailUriAsString) {
- return new RecipientEntry(
- ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource, displayName, destination),
- destination, destinationType, destinationLabel,
- contactId, dataId,
- (thumbnailUriAsString != null ? Uri.parse(thumbnailUriAsString) : null), false);
+ public static RecipientEntry constructGeneratedEntry(String display, String address,
+ boolean isValid) {
+ return new RecipientEntry(ENTRY_TYPE_PERSON, display, address, INVALID_DESTINATION_TYPE,
+ null, GENERATED_CONTACT, GENERATED_CONTACT, null, true, isValid);
+ }
+
+ public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource,
+ String destination, int destinationType, String destinationLabel, long contactId,
+ long dataId, Uri photoThumbnailUri, boolean isValid) {
+ return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
+ displayName, destination), destination, destinationType, destinationLabel,
+ contactId, dataId, photoThumbnailUri, true, isValid);
+ }
+
+ public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource,
+ String destination, int destinationType, String destinationLabel, long contactId,
+ long dataId, String thumbnailUriAsString, boolean isValid) {
+ return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
+ displayName, destination), destination, destinationType, destinationLabel,
+ contactId, dataId, (thumbnailUriAsString != null ? Uri.parse(thumbnailUriAsString)
+ : null), true, isValid);
+ }
+
+ public static RecipientEntry constructSecondLevelEntry(String displayName,
+ int displayNameSource, String destination, int destinationType,
+ String destinationLabel, long contactId, long dataId, String thumbnailUriAsString,
+ boolean isValid) {
+ return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
+ displayName, destination), destination, destinationType, destinationLabel,
+ contactId, dataId, (thumbnailUriAsString != null ? Uri.parse(thumbnailUriAsString)
+ : null), false, isValid);
}
public int getEntryType() {
@@ -226,4 +229,9 @@ public class RecipientEntry {
public boolean isSelectable() {
return mEntryType == ENTRY_TYPE_PERSON;
}
+
+ @Override
+ public String toString() {
+ return mDisplayName + " <" + mDestination + ">, isValid=" + mIsValid;
+ }
} \ No newline at end of file
diff --git a/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java b/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java
index 8131fc3..0571a4e 100644
--- a/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java
+++ b/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java
@@ -43,7 +43,7 @@ class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry> {
if (convertView == null) {
convertView = newView();
}
- bindView(convertView, convertView.getContext(), getItem(position));
+ bindView(convertView, getItem(position));
return convertView;
}
@@ -51,7 +51,7 @@ class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry> {
return mLayoutInflater.inflate(mLayoutId, null);
}
- private void bindView(View view, Context context, RecipientEntry entry) {
+ private static void bindView(View view, RecipientEntry entry) {
TextView display = (TextView) view.findViewById(android.R.id.title);
ImageView imageView = (ImageView) view.findViewById(android.R.id.icon);
display.setText(entry.getDisplayName());
diff --git a/chips/src/com/android/ex/chips/recipientchip/BaseRecipientChip.java b/chips/src/com/android/ex/chips/recipientchip/BaseRecipientChip.java
new file mode 100644
index 0000000..a080ee7
--- /dev/null
+++ b/chips/src/com/android/ex/chips/recipientchip/BaseRecipientChip.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 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.recipientchip;
+
+import com.android.ex.chips.RecipientEntry;
+
+/**
+ * BaseRecipientChip defines an object that contains information relevant to a
+ * particular recipient.
+ */
+interface BaseRecipientChip {
+
+ /**
+ * Set the selected state of the chip.
+ */
+ void setSelected(boolean selected);
+
+ /**
+ * Return true if the chip is selected.
+ */
+ boolean isSelected();
+
+ /**
+ * Get the text displayed in the chip.
+ */
+ CharSequence getDisplay();
+
+ /**
+ * Get the text value this chip represents.
+ */
+ CharSequence getValue();
+
+ /**
+ * Get the id of the contact associated with this chip.
+ */
+ long getContactId();
+
+ /**
+ * Get the id of the data associated with this chip.
+ */
+ long getDataId();
+
+ /**
+ * Get associated RecipientEntry.
+ */
+ RecipientEntry getEntry();
+
+ /**
+ * Set the text in the edittextview originally associated with this chip
+ * before any reverse lookups.
+ */
+ void setOriginalText(String text);
+
+ /**
+ * Set the text in the edittextview originally associated with this chip
+ * before any reverse lookups.
+ */
+ CharSequence getOriginalText();
+}
diff --git a/chips/src/com/android/ex/chips/recipientchip/DrawableRecipientChip.java b/chips/src/com/android/ex/chips/recipientchip/DrawableRecipientChip.java
new file mode 100644
index 0000000..396a8ac
--- /dev/null
+++ b/chips/src/com/android/ex/chips/recipientchip/DrawableRecipientChip.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 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.recipientchip;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+/**
+ * RecipientChip defines a drawable object that contains information relevant to a
+ * particular recipient.
+ */
+public interface DrawableRecipientChip extends BaseRecipientChip {
+ /**
+ * Get the bounds of the chip; may be 0,0 if it is not visibly rendered.
+ */
+ Rect getBounds();
+
+ /**
+ * Draw the chip.
+ */
+ void draw(Canvas canvas);
+}
diff --git a/chips/src/com/android/ex/chips/recipientchip/InvisibleRecipientChip.java b/chips/src/com/android/ex/chips/recipientchip/InvisibleRecipientChip.java
new file mode 100644
index 0000000..0380a81
--- /dev/null
+++ b/chips/src/com/android/ex/chips/recipientchip/InvisibleRecipientChip.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2011 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.recipientchip;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.style.ReplacementSpan;
+
+import com.android.ex.chips.RecipientEntry;
+
+/**
+ * RecipientChip defines a span that contains information relevant to a
+ * particular recipient.
+ */
+public class InvisibleRecipientChip extends ReplacementSpan implements DrawableRecipientChip {
+ private final SimpleRecipientChip mDelegate;
+
+ public InvisibleRecipientChip(final RecipientEntry entry) {
+ super();
+
+ mDelegate = new SimpleRecipientChip(entry);
+ }
+
+ @Override
+ public void setSelected(final boolean selected) {
+ mDelegate.setSelected(selected);
+ }
+
+ @Override
+ public boolean isSelected() {
+ return mDelegate.isSelected();
+ }
+
+ @Override
+ public CharSequence getDisplay() {
+ return mDelegate.getDisplay();
+ }
+
+ @Override
+ public CharSequence getValue() {
+ return mDelegate.getValue();
+ }
+
+ @Override
+ public long getContactId() {
+ return mDelegate.getContactId();
+ }
+
+ @Override
+ public long getDataId() {
+ return mDelegate.getDataId();
+ }
+
+ @Override
+ public RecipientEntry getEntry() {
+ return mDelegate.getEntry();
+ }
+
+ @Override
+ public void setOriginalText(final String text) {
+ mDelegate.setOriginalText(text);
+ }
+
+ @Override
+ public CharSequence getOriginalText() {
+ return mDelegate.getOriginalText();
+ }
+
+ @Override
+ public void draw(final Canvas canvas, final CharSequence text, final int start, final int end,
+ final float x, final int top, final int y, final int bottom, final Paint paint) {
+ // Do nothing.
+ }
+
+ @Override
+ public int getSize(final Paint paint, final CharSequence text, final int start, final int end,
+ final Paint.FontMetricsInt fm) {
+ return 0;
+ }
+
+ @Override
+ public Rect getBounds() {
+ return new Rect(0, 0, 0, 0);
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ // do nothing.
+ }
+}
diff --git a/chips/src/com/android/ex/chips/RecipientChip.java b/chips/src/com/android/ex/chips/recipientchip/SimpleRecipientChip.java
index 0f93a0d..c04b3be 100644
--- a/chips/src/com/android/ex/chips/RecipientChip.java
+++ b/chips/src/com/android/ex/chips/recipientchip/SimpleRecipientChip.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 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.
@@ -14,18 +14,13 @@
* limitations under the License.
*/
-package com.android.ex.chips;
+package com.android.ex.chips.recipientchip;
+
+import com.android.ex.chips.RecipientEntry;
-import android.graphics.drawable.Drawable;
import android.text.TextUtils;
-import android.text.style.DynamicDrawableSpan;
-import android.text.style.ImageSpan;
-/**
- * RecipientChip defines an ImageSpan that contains information relevant to a
- * particular recipient.
- */
-/* package */class RecipientChip extends ImageSpan {
+class SimpleRecipientChip implements BaseRecipientChip {
private final CharSequence mDisplay;
private final CharSequence mValue;
@@ -34,14 +29,13 @@ import android.text.style.ImageSpan;
private final long mDataId;
- private RecipientEntry mEntry;
+ private final RecipientEntry mEntry;
private boolean mSelected = false;
private CharSequence mOriginalText;
- public RecipientChip(Drawable drawable, RecipientEntry entry, int offset) {
- super(drawable, DynamicDrawableSpan.ALIGN_BOTTOM);
+ public SimpleRecipientChip(final RecipientEntry entry) {
mDisplay = entry.getDisplayName();
mValue = entry.getDestination().trim();
mContactId = entry.getContactId();
@@ -49,64 +43,57 @@ import android.text.style.ImageSpan;
mEntry = entry;
}
- /**
- * Set the selected state of the chip.
- * @param selected
- */
- public void setSelected(boolean selected) {
+ @Override
+ public void setSelected(final boolean selected) {
mSelected = selected;
}
- /**
- * Return true if the chip is selected.
- */
+ @Override
public boolean isSelected() {
return mSelected;
}
- /**
- * Get the text displayed in the chip.
- */
+ @Override
public CharSequence getDisplay() {
return mDisplay;
}
- /**
- * Get the text value this chip represents.
- */
+ @Override
public CharSequence getValue() {
return mValue;
}
- /**
- * Get the id of the contact associated with this chip.
- */
+ @Override
public long getContactId() {
return mContactId;
}
- /**
- * Get the id of the data associated with this chip.
- */
+ @Override
public long getDataId() {
return mDataId;
}
- /**
- * Get associated RecipientEntry.
- */
+ @Override
public RecipientEntry getEntry() {
return mEntry;
}
- public void setOriginalText(String text) {
- if (!TextUtils.isEmpty(text)) {
- text = text.trim();
+ @Override
+ public void setOriginalText(final String text) {
+ if (TextUtils.isEmpty(text)) {
+ mOriginalText = text;
+ } else {
+ mOriginalText = text.trim();
}
- mOriginalText = text;
}
+ @Override
public CharSequence getOriginalText() {
return !TextUtils.isEmpty(mOriginalText) ? mOriginalText : mEntry.getDestination();
}
-}
+
+ @Override
+ public String toString() {
+ return mDisplay + " <" + mValue + ">";
+ }
+} \ No newline at end of file
diff --git a/chips/src/com/android/ex/chips/recipientchip/VisibleRecipientChip.java b/chips/src/com/android/ex/chips/recipientchip/VisibleRecipientChip.java
new file mode 100644
index 0000000..acade7f
--- /dev/null
+++ b/chips/src/com/android/ex/chips/recipientchip/VisibleRecipientChip.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2011 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.recipientchip;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.style.DynamicDrawableSpan;
+import android.text.style.ImageSpan;
+
+import com.android.ex.chips.RecipientEntry;
+
+/**
+ * VisibleRecipientChip defines an ImageSpan that contains information relevant to a
+ * particular recipient and renders a background asset to go with it.
+ */
+public class VisibleRecipientChip extends ImageSpan implements DrawableRecipientChip {
+ private final SimpleRecipientChip mDelegate;
+
+ public VisibleRecipientChip(final Drawable drawable, final RecipientEntry entry) {
+ super(drawable, DynamicDrawableSpan.ALIGN_BOTTOM);
+
+ mDelegate = new SimpleRecipientChip(entry);
+ }
+
+ @Override
+ public void setSelected(final boolean selected) {
+ mDelegate.setSelected(selected);
+ }
+
+ @Override
+ public boolean isSelected() {
+ return mDelegate.isSelected();
+ }
+
+ @Override
+ public CharSequence getDisplay() {
+ return mDelegate.getDisplay();
+ }
+
+ @Override
+ public CharSequence getValue() {
+ return mDelegate.getValue();
+ }
+
+ @Override
+ public long getContactId() {
+ return mDelegate.getContactId();
+ }
+
+ @Override
+ public long getDataId() {
+ return mDelegate.getDataId();
+ }
+
+ @Override
+ public RecipientEntry getEntry() {
+ return mDelegate.getEntry();
+ }
+
+ @Override
+ public void setOriginalText(final String text) {
+ mDelegate.setOriginalText(text);
+ }
+
+ @Override
+ public CharSequence getOriginalText() {
+ return mDelegate.getOriginalText();
+ }
+
+ @Override
+ public Rect getBounds() {
+ return getDrawable().getBounds();
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ getDrawable().draw(canvas);
+ }
+
+ @Override
+ public String toString() {
+ return mDelegate.toString();
+ }
+}