diff options
Diffstat (limited to 'chips/src/com/android/ex/chips')
4 files changed, 286 insertions, 134 deletions
diff --git a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java index b6d4fa8..f742cf1 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"); @@ -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; @@ -556,8 +556,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 +595,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); @@ -708,6 +709,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; } @@ -975,11 +981,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/InvisibleRecipientChip.java b/chips/src/com/android/ex/chips/InvisibleRecipientChip.java index 46fe19a..beeb499 100644 --- a/chips/src/com/android/ex/chips/InvisibleRecipientChip.java +++ b/chips/src/com/android/ex/chips/InvisibleRecipientChip.java @@ -42,6 +42,9 @@ import android.text.style.ReplacementSpan; private CharSequence mOriginalText; + /** <code>true</code> to display the original text, <code>false</code> to display nothing */ + private boolean mDisplayOriginalText = false; + public InvisibleRecipientChip(RecipientEntry entry, int offset) { super(); mDisplay = entry.getDisplayName(); @@ -112,15 +115,28 @@ import android.text.style.ReplacementSpan; return !TextUtils.isEmpty(mOriginalText) ? mOriginalText : mEntry.getDestination(); } + public void setDisplayOriginalText(final boolean displayOriginalText) { + mDisplayOriginalText = displayOriginalText; + } + @Override - public void draw(Canvas arg0, CharSequence arg1, int arg2, int arg3, float arg4, int arg5, - int arg6, int arg7, Paint arg8) { - // Do nothing. + public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, + int bottom, Paint paint) { + if (mDisplayOriginalText) { + canvas.drawText(text, start, end, x, y, paint); + } else { + // Do nothing. + } } @Override - public int getSize(Paint arg0, CharSequence arg1, int arg2, int arg3, FontMetricsInt arg4) { - return 0; + public int getSize( + Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { + if (mDisplayOriginalText) { + return (int) paint.measureText(text, start, end); + } else { + return 0; + } } @Override diff --git a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java b/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java index b54d83d..00f1ff4 100644 --- a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java +++ b/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java @@ -16,9 +16,13 @@ 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.util.Rfc822Token; import android.text.util.Rfc822Tokenizer; import android.util.Log; @@ -29,11 +33,14 @@ 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; /** * RecipientAlternatesAdapter backs the RecipientEditTextView for managing contacts @@ -55,9 +62,13 @@ 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(HashMap<String, RecipientEntry> results); + } + + public static void getMatchingRecipients(Context context, ArrayList<String> inAddresses, + Account account, RecipientMatchCallback callback) { + getMatchingRecipients(context, inAddresses, QUERY_TYPE_EMAIL, account, callback); } /** @@ -67,10 +78,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 +90,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,44 +106,115 @@ 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) { - 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), - true)); - 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)); + 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 + if (recipientEntries.size() < addresses.size()) { + final List<DirectorySearchParams> paramsList; + Cursor directoryCursor = context.getContentResolver().query(DirectoryListQuery.URI, + DirectoryListQuery.PROJECTION, null, null, null); + paramsList = BaseRecipientAdapter.setupOtherDirectories(context, directoryCursor, + account); + // 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); + } + } + 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 { + callback.matchesFound(processContactEntries(directoryContactsCursor)); + } finally { + directoryContactsCursor.close(); + } } - } finally { - c.close(); } } + } + + 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); + 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), + true)); + 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; } + 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, int viewId, OnCheckedItemChangedListener listener) { this(context, contactId, currentId, viewId, QUERY_TYPE_EMAIL, listener); diff --git a/chips/src/com/android/ex/chips/RecipientEditTextView.java b/chips/src/com/android/ex/chips/RecipientEditTextView.java index 4b1d53f..c786126 100644 --- a/chips/src/com/android/ex/chips/RecipientEditTextView.java +++ b/chips/src/com/android/ex/chips/RecipientEditTextView.java @@ -54,7 +54,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; @@ -80,6 +79,8 @@ import android.widget.MultiAutoCompleteTextView; import android.widget.ScrollView; import android.widget.TextView; +import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -311,6 +312,8 @@ 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; } @@ -711,6 +714,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); @@ -758,7 +762,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements mActionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources() .getDisplayMetrics()); } - a.recycle(); } @@ -851,7 +854,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. @@ -1319,10 +1323,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements removeChip(mSelectedChip); } - if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) { - return true; - } - return super.onKeyDown(keyCode, event); } @@ -1904,6 +1904,13 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements end = chipEnd = Math.min(editable.length(), chipStart + token.length()); // Only set the span if we found a matching token. if (chipStart != -1) { + if (chip instanceof InvisibleRecipientChip) { + /* + * We want the original text to be displayed until we can replace it + * with a real chip + */ + ((InvisibleRecipientChip) chip).setDisplayOriginalText(true); + } editable.setSpan(chip, chipStart, chipEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -2256,6 +2263,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; + } + } } } @@ -2434,54 +2449,80 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements 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); - } - } - if (replacements != null && replacements.size() > 0) { - mHandler.post(new Runnable() { - @Override - public void run() { - Editable oldText = getText(); - int start, end; - 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()); + RecipientAlternatesAdapter.getMatchingRecipients(getContext(), addresses, + ((BaseRecipientAdapter) getAdapter()).getAccount(), + new RecipientMatchCallback() { + + @Override + public void matchesFound(HashMap<String, RecipientEntry> entries) { + 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(null); + } + } + if (replacements != null && replacements.size() > 0) { + mHandler.post(new Runnable() { + @Override + public void run() { + Editable oldText = getText(); + int start, end; + 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) { + RecipientChip replacement = replacements.get(i); + if (replacement != null) { + // Replacing the entirety of + // what the chip + // represented, including + // the extra space dividing + // it from other chips. + end = oldText.getSpanEnd(chip) + 1; + oldText.removeSpan(chip); + // 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()); + replacements.set(i, null); + } + } + i++; + } + } + }); } - i++; } - originalRecipients.clear(); - } - }); - } + }); return null; } } @@ -2503,30 +2544,40 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements 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(HashMap<String, RecipientEntry> entries) { + 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); + } + }); + } + } } - }); - } - } - } + } + + }); return null; } } |