diff options
author | Scott Kennedy <skennedy@google.com> | 2014-03-26 13:51:48 -0700 |
---|---|---|
committer | Scott Kennedy <skennedy@google.com> | 2014-03-26 13:51:48 -0700 |
commit | 0b0cefbe9f1e37e3d2300909792d1263545a7521 (patch) | |
tree | 3063b035c58fa2f01ad6d4d78af689a5c2b3098d /chips/src/com/android | |
parent | b468c8fc7e914220c457e390b23a54baee1a1c69 (diff) | |
download | android_frameworks_ex-0b0cefbe9f1e37e3d2300909792d1263545a7521.tar.gz android_frameworks_ex-0b0cefbe9f1e37e3d2300909792d1263545a7521.tar.bz2 android_frameworks_ex-0b0cefbe9f1e37e3d2300909792d1263545a7521.zip |
Remove chips from frameworks/ex
It's now its own project in frameworks/opt/chips
Change-Id: I0fc5731e3eaff21b5b026746f91b9cdace90f214
Diffstat (limited to 'chips/src/com/android')
14 files changed, 0 insertions, 5769 deletions
diff --git a/chips/src/com/android/ex/chips/AccountSpecifier.java b/chips/src/com/android/ex/chips/AccountSpecifier.java deleted file mode 100644 index 5eb8314..0000000 --- a/chips/src/com/android/ex/chips/AccountSpecifier.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; - -import android.accounts.Account; - -/** - * The AccountSpecificAdapter interface describes an Adapter - * that can take an account to retrieve information tied to - * a specific account. - */ -public interface AccountSpecifier { - public void setAccount(Account account); -} diff --git a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java deleted file mode 100644 index 468e168..0000000 --- a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java +++ /dev/null @@ -1,1005 +0,0 @@ -/* - * 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; - -import android.accounts.Account; -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Message; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Photo; -import android.provider.ContactsContract.Directory; -import android.support.v4.util.LruCache; -import android.text.TextUtils; -import android.text.util.Rfc822Token; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AutoCompleteTextView; -import android.widget.BaseAdapter; -import android.widget.Filter; -import android.widget.Filterable; - -import com.android.ex.chips.DropdownChipLayouter.AdapterType; - -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Adapter for showing a recipient list. - */ -public class BaseRecipientAdapter extends BaseAdapter implements Filterable, AccountSpecifier { - private static final String TAG = "BaseRecipientAdapter"; - - private static final boolean DEBUG = false; - - /** - * The preferred number of results to be retrieved. This number may be - * exceeded if there are several directories configured, because we will use - * the same limit for all directories. - */ - private static final int DEFAULT_PREFERRED_MAX_RESULT_COUNT = 10; - - /** - * The number of extra entries requested to allow for duplicates. Duplicates - * are removed from the overall result. - */ - static final int ALLOWANCE_FOR_DUPLICATES = 5; - - // This is ContactsContract.PRIMARY_ACCOUNT_NAME. Available from ICS as hidden - static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account"; - // This is ContactsContract.PRIMARY_ACCOUNT_TYPE. Available from ICS as hidden - static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account"; - - /** The number of photos cached in this Adapter. */ - private static final int PHOTO_CACHE_SIZE = 20; - - /** - * The "Waiting for more contacts" message will be displayed if search is not complete - * within this many milliseconds. - */ - private static final int MESSAGE_SEARCH_PENDING_DELAY = 1000; - /** Used to prepare "Waiting for more contacts" message. */ - private static final int MESSAGE_SEARCH_PENDING = 1; - - public static final int QUERY_TYPE_EMAIL = 0; - public static final int QUERY_TYPE_PHONE = 1; - - private final Queries.Query mQuery; - private final int mQueryType; - - /** - * Model object for a {@link Directory} row. - */ - public final static class DirectorySearchParams { - public long directoryId; - public String directoryType; - public String displayName; - public String accountName; - public String accountType; - public CharSequence constraint; - public DirectoryFilter filter; - } - - private static class PhotoQuery { - public static final String[] PROJECTION = { - Photo.PHOTO - }; - - public static final int PHOTO = 0; - } - - protected static class DirectoryListQuery { - - public static final Uri URI = - Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories"); - public static final String[] PROJECTION = { - Directory._ID, // 0 - Directory.ACCOUNT_NAME, // 1 - Directory.ACCOUNT_TYPE, // 2 - Directory.DISPLAY_NAME, // 3 - Directory.PACKAGE_NAME, // 4 - Directory.TYPE_RESOURCE_ID, // 5 - }; - - public static final int ID = 0; - public static final int ACCOUNT_NAME = 1; - public static final int ACCOUNT_TYPE = 2; - public static final int DISPLAY_NAME = 3; - public static final int PACKAGE_NAME = 4; - public static final int TYPE_RESOURCE_ID = 5; - } - - /** Used to temporarily hold results in Cursor objects. */ - protected static class TemporaryEntry { - public final String displayName; - public final String destination; - public final int destinationType; - public final String destinationLabel; - public final long contactId; - public final Long directoryId; - public final long dataId; - public final String thumbnailUriString; - public final int displayNameSource; - public final String lookupKey; - - public TemporaryEntry( - String displayName, - String destination, - int destinationType, - String destinationLabel, - long contactId, - Long directoryId, - long dataId, - String thumbnailUriString, - int displayNameSource, - String lookupKey) { - this.displayName = displayName; - this.destination = destination; - this.destinationType = destinationType; - this.destinationLabel = destinationLabel; - this.contactId = contactId; - this.directoryId = directoryId; - this.dataId = dataId; - this.thumbnailUriString = thumbnailUriString; - this.displayNameSource = displayNameSource; - this.lookupKey = lookupKey; - } - - public TemporaryEntry(Cursor cursor, Long directoryId) { - this.displayName = cursor.getString(Queries.Query.NAME); - this.destination = cursor.getString(Queries.Query.DESTINATION); - this.destinationType = cursor.getInt(Queries.Query.DESTINATION_TYPE); - this.destinationLabel = cursor.getString(Queries.Query.DESTINATION_LABEL); - this.contactId = cursor.getLong(Queries.Query.CONTACT_ID); - this.directoryId = directoryId; - this.dataId = cursor.getLong(Queries.Query.DATA_ID); - this.thumbnailUriString = cursor.getString(Queries.Query.PHOTO_THUMBNAIL_URI); - this.displayNameSource = cursor.getInt(Queries.Query.DISPLAY_NAME_SOURCE); - this.lookupKey = cursor.getString(Queries.Query.LOOKUP_KEY); - } - } - - /** - * Used to pass results from {@link DefaultFilter#performFiltering(CharSequence)} to - * {@link DefaultFilter#publishResults(CharSequence, android.widget.Filter.FilterResults)} - */ - private static class DefaultFilterResult { - public final List<RecipientEntry> entries; - public final LinkedHashMap<Long, List<RecipientEntry>> entryMap; - public final List<RecipientEntry> nonAggregatedEntries; - public final Set<String> existingDestinations; - public final List<DirectorySearchParams> paramsList; - - public DefaultFilterResult(List<RecipientEntry> entries, - LinkedHashMap<Long, List<RecipientEntry>> entryMap, - List<RecipientEntry> nonAggregatedEntries, - Set<String> existingDestinations, - List<DirectorySearchParams> paramsList) { - this.entries = entries; - this.entryMap = entryMap; - this.nonAggregatedEntries = nonAggregatedEntries; - this.existingDestinations = existingDestinations; - this.paramsList = paramsList; - } - } - - /** - * An asynchronous filter used for loading two data sets: email rows from the local - * contact provider and the list of {@link Directory}'s. - */ - private final class DefaultFilter extends Filter { - - @Override - protected FilterResults performFiltering(CharSequence constraint) { - if (DEBUG) { - Log.d(TAG, "start filtering. constraint: " + constraint + ", thread:" - + Thread.currentThread()); - } - - final FilterResults results = new FilterResults(); - Cursor defaultDirectoryCursor = null; - Cursor directoryCursor = null; - - if (TextUtils.isEmpty(constraint)) { - clearTempEntries(); - // Return empty results. - return results; - } - - try { - defaultDirectoryCursor = doQuery(constraint, mPreferredMaxResultCount, - null /* directoryId */); - - if (defaultDirectoryCursor == null) { - if (DEBUG) { - Log.w(TAG, "null cursor returned for default Email filter query."); - } - } else { - // These variables will become mEntries, mEntryMap, mNonAggregatedEntries, and - // mExistingDestinations. Here we shouldn't use those member variables directly - // since this method is run outside the UI thread. - final LinkedHashMap<Long, List<RecipientEntry>> entryMap = - new LinkedHashMap<Long, List<RecipientEntry>>(); - final List<RecipientEntry> nonAggregatedEntries = - new ArrayList<RecipientEntry>(); - final Set<String> existingDestinations = new HashSet<String>(); - - while (defaultDirectoryCursor.moveToNext()) { - // Note: At this point each entry doesn't contain any photo - // (thus getPhotoBytes() returns null). - putOneEntry(new TemporaryEntry(defaultDirectoryCursor, - null /* directoryId */), - true, entryMap, nonAggregatedEntries, existingDestinations); - } - - // We'll copy this result to mEntry in publicResults() (run in the UX thread). - final List<RecipientEntry> entries = constructEntryList( - entryMap, nonAggregatedEntries); - - // After having local results, check the size of results. If the results are - // not enough, we search remote directories, which will take longer time. - final int limit = mPreferredMaxResultCount - existingDestinations.size(); - final List<DirectorySearchParams> paramsList; - if (limit > 0) { - if (DEBUG) { - Log.d(TAG, "More entries should be needed (current: " - + existingDestinations.size() - + ", remaining limit: " + limit + ") "); - } - directoryCursor = mContentResolver.query( - DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, - null, null, null); - paramsList = setupOtherDirectories(mContext, directoryCursor, mAccount); - } else { - // We don't need to search other directories. - paramsList = null; - } - - results.values = new DefaultFilterResult( - entries, entryMap, nonAggregatedEntries, - existingDestinations, paramsList); - results.count = 1; - } - } finally { - if (defaultDirectoryCursor != null) { - defaultDirectoryCursor.close(); - } - if (directoryCursor != null) { - directoryCursor.close(); - } - } - return results; - } - - @Override - protected void publishResults(final CharSequence constraint, FilterResults results) { - // If a user types a string very quickly and database is slow, "constraint" refers to - // an older text which shows inconsistent results for users obsolete (b/4998713). - // TODO: Fix it. - mCurrentConstraint = constraint; - - clearTempEntries(); - - if (results.values != null) { - DefaultFilterResult defaultFilterResult = (DefaultFilterResult) results.values; - mEntryMap = defaultFilterResult.entryMap; - mNonAggregatedEntries = defaultFilterResult.nonAggregatedEntries; - mExistingDestinations = defaultFilterResult.existingDestinations; - - // If there are no local results, in the new result set, cache off what had been - // shown to the user for use until the first directory result is returned - if (defaultFilterResult.entries.size() == 0 && - defaultFilterResult.paramsList != null) { - cacheCurrentEntries(); - } - - updateEntries(defaultFilterResult.entries); - - // We need to search other remote directories, doing other Filter requests. - if (defaultFilterResult.paramsList != null) { - final int limit = mPreferredMaxResultCount - - defaultFilterResult.existingDestinations.size(); - startSearchOtherDirectories(constraint, defaultFilterResult.paramsList, limit); - } - } - - } - - @Override - public CharSequence convertResultToString(Object resultValue) { - final RecipientEntry entry = (RecipientEntry)resultValue; - final String displayName = entry.getDisplayName(); - final String emailAddress = entry.getDestination(); - if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { - return emailAddress; - } else { - return new Rfc822Token(displayName, emailAddress, null).toString(); - } - } - } - - /** - * An asynchronous filter that performs search in a particular directory. - */ - protected class DirectoryFilter extends Filter { - private final DirectorySearchParams mParams; - private int mLimit; - - public DirectoryFilter(DirectorySearchParams params) { - mParams = params; - } - - public synchronized void setLimit(int limit) { - this.mLimit = limit; - } - - public synchronized int getLimit() { - return this.mLimit; - } - - @Override - protected FilterResults performFiltering(CharSequence constraint) { - if (DEBUG) { - Log.d(TAG, "DirectoryFilter#performFiltering. directoryId: " + mParams.directoryId - + ", constraint: " + constraint + ", thread: " + Thread.currentThread()); - } - final FilterResults results = new FilterResults(); - results.values = null; - results.count = 0; - - if (!TextUtils.isEmpty(constraint)) { - final ArrayList<TemporaryEntry> tempEntries = new ArrayList<TemporaryEntry>(); - - Cursor cursor = null; - try { - // We don't want to pass this Cursor object to UI thread (b/5017608). - // Assuming the result should contain fairly small results (at most ~10), - // We just copy everything to local structure. - cursor = doQuery(constraint, getLimit(), mParams.directoryId); - - if (cursor != null) { - while (cursor.moveToNext()) { - tempEntries.add(new TemporaryEntry(cursor, mParams.directoryId)); - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - if (!tempEntries.isEmpty()) { - results.values = tempEntries; - results.count = 1; - } - } - - if (DEBUG) { - Log.v(TAG, "finished loading directory \"" + mParams.displayName + "\"" + - " with query " + constraint); - } - - return results; - } - - @Override - protected void publishResults(final CharSequence constraint, FilterResults results) { - if (DEBUG) { - Log.d(TAG, "DirectoryFilter#publishResult. constraint: " + constraint - + ", mCurrentConstraint: " + mCurrentConstraint); - } - mDelayedMessageHandler.removeDelayedLoadMessage(); - // Check if the received result matches the current constraint - // If not - the user must have continued typing after the request was issued, which - // means several member variables (like mRemainingDirectoryLoad) are already - // overwritten so shouldn't be touched here anymore. - if (TextUtils.equals(constraint, mCurrentConstraint)) { - if (results.count > 0) { - @SuppressWarnings("unchecked") - final ArrayList<TemporaryEntry> tempEntries = - (ArrayList<TemporaryEntry>) results.values; - - for (TemporaryEntry tempEntry : tempEntries) { - putOneEntry(tempEntry, mParams.directoryId == Directory.DEFAULT, - mEntryMap, mNonAggregatedEntries, mExistingDestinations); - } - } - - // If there are remaining directories, set up delayed message again. - mRemainingDirectoryCount--; - if (mRemainingDirectoryCount > 0) { - if (DEBUG) { - Log.d(TAG, "Resend delayed load message. Current mRemainingDirectoryLoad: " - + mRemainingDirectoryCount); - } - mDelayedMessageHandler.sendDelayedLoadMessage(); - } - - // If this directory result has some items, or there are no more directories that - // we are waiting for, clear the temp results - if (results.count > 0 || mRemainingDirectoryCount == 0) { - // Clear the temp entries - clearTempEntries(); - } - } - - // Show the list again without "waiting" message. - updateEntries(constructEntryList(mEntryMap, mNonAggregatedEntries)); - } - } - - private final Context mContext; - private final ContentResolver mContentResolver; - private final LayoutInflater mInflater; - private Account mAccount; - private final int mPreferredMaxResultCount; - private DropdownChipLayouter mDropdownChipLayouter; - - /** - * {@link #mEntries} is responsible for showing every result for this Adapter. To - * construct it, we use {@link #mEntryMap}, {@link #mNonAggregatedEntries}, and - * {@link #mExistingDestinations}. - * - * First, each destination (an email address or a phone number) with a valid contactId is - * inserted into {@link #mEntryMap} and grouped by the contactId. Destinations without valid - * contactId (possible if they aren't in local storage) are stored in - * {@link #mNonAggregatedEntries}. - * Duplicates are removed using {@link #mExistingDestinations}. - * - * After having all results from Cursor objects, all destinations in mEntryMap are copied to - * {@link #mEntries}. If the number of destinations is not enough (i.e. less than - * {@link #mPreferredMaxResultCount}), destinations in mNonAggregatedEntries are also used. - * - * These variables are only used in UI thread, thus should not be touched in - * performFiltering() methods. - */ - private LinkedHashMap<Long, List<RecipientEntry>> mEntryMap; - private List<RecipientEntry> mNonAggregatedEntries; - private Set<String> mExistingDestinations; - /** Note: use {@link #updateEntries(List)} to update this variable. */ - private List<RecipientEntry> mEntries; - private List<RecipientEntry> mTempEntries; - - /** The number of directories this adapter is waiting for results. */ - private int mRemainingDirectoryCount; - - /** - * Used to ignore asynchronous queries with a different constraint, which may happen when - * users type characters quickly. - */ - private CharSequence mCurrentConstraint; - - private final LruCache<Uri, byte[]> mPhotoCacheMap; - - /** - * Handler specific for maintaining "Waiting for more contacts" message, which will be shown - * when: - * - there are directories to be searched - * - results from directories are slow to come - */ - private final class DelayedMessageHandler extends Handler { - @Override - public void handleMessage(Message msg) { - if (mRemainingDirectoryCount > 0) { - updateEntries(constructEntryList(mEntryMap, mNonAggregatedEntries)); - } - } - - public void sendDelayedLoadMessage() { - sendMessageDelayed(obtainMessage(MESSAGE_SEARCH_PENDING, 0, 0, null), - MESSAGE_SEARCH_PENDING_DELAY); - } - - public void removeDelayedLoadMessage() { - removeMessages(MESSAGE_SEARCH_PENDING); - } - } - - private final DelayedMessageHandler mDelayedMessageHandler = new DelayedMessageHandler(); - - private EntriesUpdatedObserver mEntriesUpdatedObserver; - - /** - * Constructor for email queries. - */ - public BaseRecipientAdapter(Context context) { - this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, QUERY_TYPE_EMAIL); - } - - public BaseRecipientAdapter(Context context, int preferredMaxResultCount) { - this(context, preferredMaxResultCount, QUERY_TYPE_EMAIL); - } - - public BaseRecipientAdapter(int queryMode, Context context) { - this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, queryMode); - } - - public BaseRecipientAdapter(int queryMode, Context context, int preferredMaxResultCount) { - this(context, preferredMaxResultCount, queryMode); - } - - public BaseRecipientAdapter(Context context, int preferredMaxResultCount, int queryMode) { - mContext = context; - mContentResolver = context.getContentResolver(); - mInflater = LayoutInflater.from(context); - mPreferredMaxResultCount = preferredMaxResultCount; - mPhotoCacheMap = new LruCache<Uri, byte[]>(PHOTO_CACHE_SIZE); - mQueryType = queryMode; - - if (queryMode == QUERY_TYPE_EMAIL) { - mQuery = Queries.EMAIL; - } else if (queryMode == QUERY_TYPE_PHONE) { - mQuery = Queries.PHONE; - } else { - mQuery = Queries.EMAIL; - Log.e(TAG, "Unsupported query type: " + queryMode); - } - } - - public Context getContext() { - return mContext; - } - - public int getQueryType() { - return mQueryType; - } - - public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) { - mDropdownChipLayouter = dropdownChipLayouter; - mDropdownChipLayouter.setQuery(mQuery); - } - - public DropdownChipLayouter getDropdownChipLayouter() { - return mDropdownChipLayouter; - } - - /** - * Set the account when known. Causes the search to prioritize contacts from that account. - */ - @Override - public void setAccount(Account account) { - mAccount = account; - } - - /** Will be called from {@link AutoCompleteTextView} to prepare auto-complete list. */ - @Override - public Filter getFilter() { - return new DefaultFilter(); - } - - /** - * An extesion to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows - * additional sources of contacts to be considered as matching recipients. - * @param addresses A set of addresses to be matched - * @return A list of matches or null if none found - */ - public Map<String, RecipientEntry> getMatchingRecipients(Set<String> addresses) { - return null; - } - - 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()) { - final long id = directoryCursor.getLong(DirectoryListQuery.ID); - - // Skip the local invisible directory, because the default directory already includes - // all local results. - if (id == Directory.LOCAL_INVISIBLE) { - continue; - } - - final DirectorySearchParams params = new DirectorySearchParams(); - final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); - final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); - params.directoryId = id; - params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); - params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); - params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); - if (packageName != null && resourceId != 0) { - try { - final Resources resources = - packageManager.getResourcesForApplication(packageName); - params.directoryType = resources.getString(resourceId); - if (params.directoryType == null) { - Log.e(TAG, "Cannot resolve directory name: " - + resourceId + "@" + packageName); - } - } catch (NameNotFoundException e) { - Log.e(TAG, "Cannot resolve directory name: " - + resourceId + "@" + packageName, e); - } - } - - // 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 (account != null && account.name.equals(params.accountName) && - account.type.equals(params.accountType)) { - preferredDirectory = params; - } else { - paramsList.add(params); - } - } - - if (preferredDirectory != null) { - paramsList.add(1, preferredDirectory); - } - - return paramsList; - } - - /** - * Starts search in other directories using {@link Filter}. Results will be handled in - * {@link DirectoryFilter}. - */ - protected void startSearchOtherDirectories( - CharSequence constraint, List<DirectorySearchParams> paramsList, int limit) { - final int count = paramsList.size(); - // Note: skipping the default partition (index 0), which has already been loaded - for (int i = 1; i < count; i++) { - final DirectorySearchParams params = paramsList.get(i); - params.constraint = constraint; - if (params.filter == null) { - params.filter = new DirectoryFilter(params); - } - params.filter.setLimit(limit); - params.filter.filter(constraint); - } - - // Directory search started. We may show "waiting" message if directory results are slow - // enough. - mRemainingDirectoryCount = count - 1; - mDelayedMessageHandler.sendDelayedLoadMessage(); - } - - private static void putOneEntry(TemporaryEntry entry, boolean isAggregatedEntry, - LinkedHashMap<Long, List<RecipientEntry>> entryMap, - List<RecipientEntry> nonAggregatedEntries, - Set<String> existingDestinations) { - if (existingDestinations.contains(entry.destination)) { - return; - } - - existingDestinations.add(entry.destination); - - if (!isAggregatedEntry) { - nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry( - entry.displayName, - entry.displayNameSource, - entry.destination, entry.destinationType, entry.destinationLabel, - entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, - true, entry.lookupKey)); - } else if (entryMap.containsKey(entry.contactId)) { - // We already have a section for the person. - final List<RecipientEntry> entryList = entryMap.get(entry.contactId); - entryList.add(RecipientEntry.constructSecondLevelEntry( - entry.displayName, - entry.displayNameSource, - entry.destination, entry.destinationType, entry.destinationLabel, - entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString, - true, entry.lookupKey)); - } 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.directoryId, entry.dataId, entry.thumbnailUriString, - true, entry.lookupKey)); - entryMap.put(entry.contactId, entryList); - } - } - - /** - * Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to - * fetch a cached photo for each contact entry (other than separators), or request another - * thread to get one from directories. - */ - private List<RecipientEntry> constructEntryList( - LinkedHashMap<Long, List<RecipientEntry>> entryMap, - List<RecipientEntry> nonAggregatedEntries) { - final List<RecipientEntry> entries = new ArrayList<RecipientEntry>(); - int validEntryCount = 0; - for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) { - final List<RecipientEntry> entryList = mapEntry.getValue(); - final int size = entryList.size(); - for (int i = 0; i < size; i++) { - RecipientEntry entry = entryList.get(i); - entries.add(entry); - tryFetchPhoto(entry); - validEntryCount++; - } - if (validEntryCount > mPreferredMaxResultCount) { - break; - } - } - if (validEntryCount <= mPreferredMaxResultCount) { - for (RecipientEntry entry : nonAggregatedEntries) { - if (validEntryCount > mPreferredMaxResultCount) { - break; - } - entries.add(entry); - tryFetchPhoto(entry); - - validEntryCount++; - } - } - - return entries; - } - - - public interface EntriesUpdatedObserver { - public void onChanged(List<RecipientEntry> entries); - } - - public void registerUpdateObserver(EntriesUpdatedObserver observer) { - mEntriesUpdatedObserver = observer; - } - - /** Resets {@link #mEntries} and notify the event to its parent ListView. */ - private void updateEntries(List<RecipientEntry> newEntries) { - mEntries = newEntries; - mEntriesUpdatedObserver.onChanged(newEntries); - notifyDataSetChanged(); - } - - private void cacheCurrentEntries() { - mTempEntries = mEntries; - } - - private void clearTempEntries() { - mTempEntries = null; - } - - protected List<RecipientEntry> getEntries() { - return mTempEntries != null ? mTempEntries : mEntries; - } - - private void tryFetchPhoto(final RecipientEntry entry) { - final Uri photoThumbnailUri = entry.getPhotoThumbnailUri(); - if (photoThumbnailUri != null) { - final byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri); - if (photoBytes != null) { - entry.setPhotoBytes(photoBytes); - // notifyDataSetChanged() should be called by a caller. - } else { - if (DEBUG) { - Log.d(TAG, "No photo cache for " + entry.getDisplayName() - + ". Fetch one asynchronously"); - } - fetchPhotoAsync(entry, photoThumbnailUri); - } - } - } - - // For reading photos for directory contacts, this is the chunksize for - // copying from the inputstream to the output stream. - private static final int BUFFER_SIZE = 1024*16; - - private void fetchPhotoAsync(final RecipientEntry entry, final Uri photoThumbnailUri) { - final AsyncTask<Void, Void, byte[]> photoLoadTask = new AsyncTask<Void, Void, byte[]>() { - @Override - protected byte[] doInBackground(Void... params) { - // First try running a query. Images for local contacts are - // loaded by sending a query to the ContactsProvider. - final Cursor photoCursor = mContentResolver.query( - photoThumbnailUri, PhotoQuery.PROJECTION, null, null, null); - if (photoCursor != null) { - try { - if (photoCursor.moveToFirst()) { - return photoCursor.getBlob(PhotoQuery.PHOTO); - } - } finally { - photoCursor.close(); - } - } else { - // If the query fails, try streaming the URI directly. - // For remote directory images, this URI resolves to the - // directory provider and the images are loaded by sending - // an openFile call to the provider. - try { - InputStream is = mContentResolver.openInputStream( - photoThumbnailUri); - if (is != null) { - byte[] buffer = new byte[BUFFER_SIZE]; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - int size; - while ((size = is.read(buffer)) != -1) { - baos.write(buffer, 0, size); - } - } finally { - is.close(); - } - return baos.toByteArray(); - } - } catch (IOException ex) { - // ignore - } - } - return null; - } - - @Override - protected void onPostExecute(final byte[] photoBytes) { - entry.setPhotoBytes(photoBytes); - if (photoBytes != null) { - mPhotoCacheMap.put(photoThumbnailUri, photoBytes); - notifyDataSetChanged(); - } - } - }; - photoLoadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); - } - - protected void fetchPhoto(final RecipientEntry entry, final Uri photoThumbnailUri) { - byte[] photoBytes = mPhotoCacheMap.get(photoThumbnailUri); - if (photoBytes != null) { - entry.setPhotoBytes(photoBytes); - return; - } - final Cursor photoCursor = mContentResolver.query(photoThumbnailUri, PhotoQuery.PROJECTION, - null, null, null); - if (photoCursor != null) { - try { - if (photoCursor.moveToFirst()) { - photoBytes = photoCursor.getBlob(PhotoQuery.PHOTO); - entry.setPhotoBytes(photoBytes); - mPhotoCacheMap.put(photoThumbnailUri, photoBytes); - } - } finally { - photoCursor.close(); - } - } else { - InputStream inputStream = null; - ByteArrayOutputStream outputStream = null; - try { - inputStream = mContentResolver.openInputStream(photoThumbnailUri); - final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); - - if (bitmap != null) { - outputStream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); - photoBytes = outputStream.toByteArray(); - - entry.setPhotoBytes(photoBytes); - mPhotoCacheMap.put(photoThumbnailUri, photoBytes); - } - } catch (final FileNotFoundException e) { - Log.w(TAG, "Error opening InputStream for photo", e); - } finally { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - Log.e(TAG, "Error closing photo input stream", e); - } - try { - if (outputStream != null) { - outputStream.close(); - } - } catch (IOException e) { - Log.e(TAG, "Error closing photo output stream", e); - } - } - } - } - - private Cursor doQuery(CharSequence constraint, int limit, Long directoryId) { - final Uri.Builder builder = mQuery.getContentFilterUri().buildUpon() - .appendPath(constraint.toString()) - .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, - String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES)); - if (directoryId != null) { - builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, - String.valueOf(directoryId)); - } - if (mAccount != null) { - builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name); - builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type); - } - final long start = System.currentTimeMillis(); - final Cursor cursor = mContentResolver.query( - builder.build(), mQuery.getProjection(), null, null, null); - final long end = System.currentTimeMillis(); - if (DEBUG) { - Log.d(TAG, "Time for autocomplete (query: " + constraint - + ", directoryId: " + directoryId + ", num_of_results: " - + (cursor != null ? cursor.getCount() : "null") + "): " - + (end - start) + " ms"); - } - return cursor; - } - - // TODO: This won't be used at all. We should find better way to quit the thread.. - /*public void close() { - mEntries = null; - mPhotoCacheMap.evictAll(); - if (!sPhotoHandlerThread.quit()) { - Log.w(TAG, "Failed to quit photo handler thread, ignoring it."); - } - }*/ - - @Override - public int getCount() { - final List<RecipientEntry> entries = getEntries(); - return entries != null ? entries.size() : 0; - } - - @Override - public RecipientEntry getItem(int position) { - return getEntries().get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public int getViewTypeCount() { - return RecipientEntry.ENTRY_TYPE_SIZE; - } - - @Override - public int getItemViewType(int position) { - return getEntries().get(position).getEntryType(); - } - - @Override - public boolean isEnabled(int position) { - return getEntries().get(position).isSelectable(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final RecipientEntry entry = getEntries().get(position); - - final String constraint = mCurrentConstraint == null ? null : - mCurrentConstraint.toString(); - - return mDropdownChipLayouter.bindView(convertView, parent, entry, position, - AdapterType.BASE_RECIPIENT, constraint); - } - - public Account getAccount() { - return mAccount; - } -} diff --git a/chips/src/com/android/ex/chips/ChipsUtil.java b/chips/src/com/android/ex/chips/ChipsUtil.java deleted file mode 100644 index 559b2c9..0000000 --- a/chips/src/com/android/ex/chips/ChipsUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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; - -import android.os.Build; - -public class ChipsUtil { - - /** - * @return true when the caller can use Chips UI in its environment. - */ - public static boolean supportsChipsUi() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; - } -}
\ No newline at end of file diff --git a/chips/src/com/android/ex/chips/DropdownChipLayouter.java b/chips/src/com/android/ex/chips/DropdownChipLayouter.java deleted file mode 100644 index 6b0e78e..0000000 --- a/chips/src/com/android/ex/chips/DropdownChipLayouter.java +++ /dev/null @@ -1,274 +0,0 @@ -package com.android.ex.chips; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.text.TextUtils; -import android.text.util.Rfc822Tokenizer; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.ex.chips.Queries.Query; - -/** - * A class that inflates and binds the views in the dropdown list from - * RecipientEditTextView. - */ -public class DropdownChipLayouter { - /** - * The type of adapter that is requesting a chip layout. - */ - public enum AdapterType { - BASE_RECIPIENT, - RECIPIENT_ALTERNATES, - SINGLE_RECIPIENT - } - - private final LayoutInflater mInflater; - private final Context mContext; - private Query mQuery; - - public DropdownChipLayouter(LayoutInflater inflater, Context context) { - mInflater = inflater; - mContext = context; - } - - public void setQuery(Query query) { - mQuery = query; - } - - - /** - * Layouts and binds recipient information to the view. If convertView is null, inflates a new - * view with getItemLaytout(). - * - * @param convertView The view to bind information to. - * @param parent The parent to bind the view to if we inflate a new view. - * @param entry The recipient entry to get information from. - * @param position The position in the list. - * @param type The adapter type that is requesting the bind. - * @param constraint The constraint typed in the auto complete view. - * - * @return A view ready to be shown in the drop down list. - */ - public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, - AdapterType type, String constraint) { - // Default to show all the information - String displayName = entry.getDisplayName(); - String destination = entry.getDestination(); - boolean showImage = true; - CharSequence destinationType = getDestinationType(entry); - - final View itemView = reuseOrInflateView(convertView, parent, type); - - final ViewHolder viewHolder = new ViewHolder(itemView); - - // Hide some information depending on the entry type and adapter type - switch (type) { - case BASE_RECIPIENT: - if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) { - displayName = destination; - - // We only show the destination for secondary entries, so clear it only for the - // first level. - if (entry.isFirstLevel()) { - destination = null; - } - } - - if (!entry.isFirstLevel()) { - displayName = null; - showImage = false; - } - break; - case RECIPIENT_ALTERNATES: - if (position != 0) { - displayName = null; - showImage = false; - } - break; - case SINGLE_RECIPIENT: - destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress(); - destinationType = null; - } - - // Bind the information to the view - bindTextToView(displayName, viewHolder.displayNameView); - bindTextToView(destination, viewHolder.destinationView); - bindTextToView(destinationType, viewHolder.destinationTypeView); - bindIconToView(showImage, entry, viewHolder.imageView, type); - - return itemView; - } - - /** - * Returns a new view with {@link #getItemLayoutResId()}. - */ - public View newView() { - return mInflater.inflate(getItemLayoutResId(), null); - } - - /** - * Returns the same view, or inflates a new one if the given view was null. - */ - protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) { - int itemLayout = getItemLayoutResId(); - switch (type) { - case BASE_RECIPIENT: - case RECIPIENT_ALTERNATES: - break; - case SINGLE_RECIPIENT: - itemLayout = getAlternateItemLayoutResId(); - break; - } - return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false); - } - - /** - * Binds the text to the given text view. If the text was null, hides the text view. - */ - protected void bindTextToView(CharSequence text, TextView view) { - if (view == null) { - return; - } - - if (text != null) { - view.setText(text); - view.setVisibility(View.VISIBLE); - } else { - view.setVisibility(View.GONE); - } - } - - /** - * Binds the avatar icon to the image view. If we don't want to show the image, hides the - * image view. - */ - protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view, - AdapterType type) { - if (view == null) { - return; - } - - if (showImage) { - switch (type) { - case BASE_RECIPIENT: - byte[] photoBytes = entry.getPhotoBytes(); - if (photoBytes != null && photoBytes.length > 0) { - final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, - photoBytes.length); - view.setImageBitmap(photo); - } else { - view.setImageResource(getDefaultPhotoResId()); - } - break; - case RECIPIENT_ALTERNATES: - Uri thumbnailUri = entry.getPhotoThumbnailUri(); - if (thumbnailUri != null) { - // TODO: see if this needs to be done outside the main thread - // as it may be too slow to get immediately. - view.setImageURI(thumbnailUri); - } else { - view.setImageResource(getDefaultPhotoResId()); - } - break; - case SINGLE_RECIPIENT: - default: - break; - } - view.setVisibility(View.VISIBLE); - } else { - view.setVisibility(View.GONE); - } - } - - protected CharSequence getDestinationType(RecipientEntry entry) { - return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(), - entry.getDestinationLabel()).toString().toUpperCase(); - } - - /** - * Returns a layout id for each item inside auto-complete list. - * - * Each View must contain two TextViews (for display name and destination) and one ImageView - * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, - * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. - */ - protected int getItemLayoutResId() { - return R.layout.chips_recipient_dropdown_item; - } - - /** - * Returns a layout id for each item inside alternate auto-complete list. - * - * Each View must contain two TextViews (for display name and destination) and one ImageView - * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, - * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. - */ - protected int getAlternateItemLayoutResId() { - return R.layout.chips_alternate_item; - } - - /** - * Returns a resource ID representing an image which should be shown when ther's no relevant - * photo is available. - */ - protected int getDefaultPhotoResId() { - return R.drawable.ic_contact_picture; - } - - /** - * Returns an id for TextView in an item View for showing a display name. By default - * {@link android.R.id#title} is returned. - */ - protected int getDisplayNameResId() { - return android.R.id.title; - } - - /** - * Returns an id for TextView in an item View for showing a destination - * (an email address or a phone number). - * By default {@link android.R.id#text1} is returned. - */ - protected int getDestinationResId() { - return android.R.id.text1; - } - - /** - * Returns an id for TextView in an item View for showing the type of the destination. - * By default {@link android.R.id#text2} is returned. - */ - protected int getDestinationTypeResId() { - return android.R.id.text2; - } - - /** - * Returns an id for ImageView in an item View for showing photo image for a person. In default - * {@link android.R.id#icon} is returned. - */ - protected int getPhotoResId() { - return android.R.id.icon; - } - - /** - * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the - * corresponding views. - */ - protected class ViewHolder { - public final TextView displayNameView; - public final TextView destinationView; - public final TextView destinationTypeView; - public final ImageView imageView; - - public ViewHolder(View view) { - displayNameView = (TextView) view.findViewById(getDisplayNameResId()); - destinationView = (TextView) view.findViewById(getDestinationResId()); - destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId()); - imageView = (ImageView) view.findViewById(getPhotoResId()); - } - } -} diff --git a/chips/src/com/android/ex/chips/Queries.java b/chips/src/com/android/ex/chips/Queries.java deleted file mode 100644 index 1e66b96..0000000 --- a/chips/src/com/android/ex/chips/Queries.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ex.chips; - -import android.content.res.Resources; -import android.net.Uri; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Email; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.Contacts; - -/** - * Phone and Email queries for supporting Chips UI. - */ -/* package */ class Queries { - - public static final Query PHONE = new Query(new String[] { - Contacts.DISPLAY_NAME, // 0 - Phone.NUMBER, // 1 - Phone.TYPE, // 2 - Phone.LABEL, // 3 - Phone.CONTACT_ID, // 4 - Phone._ID, // 5 - Contacts.PHOTO_THUMBNAIL_URI, // 6 - Contacts.DISPLAY_NAME_SOURCE, // 7 - Contacts.LOOKUP_KEY, // 8 - ContactsContract.CommonDataKinds.Email.MIMETYPE // 9 - }, Phone.CONTENT_FILTER_URI, Phone.CONTENT_URI) { - - @Override - public CharSequence getTypeLabel(Resources res, int type, CharSequence label) { - return Phone.getTypeLabel(res, type, label); - } - - }; - - public static final Query EMAIL = new Query(new String[]{ - Contacts.DISPLAY_NAME, // 0 - Email.DATA, // 1 - Email.TYPE, // 2 - Email.LABEL, // 3 - Email.CONTACT_ID, // 4 - Email._ID, // 5 - Contacts.PHOTO_THUMBNAIL_URI, // 6 - Contacts.DISPLAY_NAME_SOURCE, // 7 - Contacts.LOOKUP_KEY, // 8 - ContactsContract.CommonDataKinds.Email.MIMETYPE // 9 - }, Email.CONTENT_FILTER_URI, Email.CONTENT_URI) { - - @Override - public CharSequence getTypeLabel(Resources res, int type, CharSequence label) { - return Email.getTypeLabel(res, type, label); - } - - }; - - static abstract class Query { - private final String[] mProjection; - private final Uri mContentFilterUri; - private final Uri mContentUri; - - public static final int NAME = 0; // String - public static final int DESTINATION = 1; // String - public static final int DESTINATION_TYPE = 2; // int - public static final int DESTINATION_LABEL = 3; // String - public static final int CONTACT_ID = 4; // long - public static final int DATA_ID = 5; // long - public static final int PHOTO_THUMBNAIL_URI = 6; // String - public static final int DISPLAY_NAME_SOURCE = 7; // int - public static final int LOOKUP_KEY = 8; // String - public static final int MIME_TYPE = 9; // String - - public Query(String[] projection, Uri contentFilter, Uri content) { - mProjection = projection; - mContentFilterUri = contentFilter; - mContentUri = content; - } - - public String[] getProjection() { - return mProjection; - } - - public Uri getContentFilterUri() { - return mContentFilterUri; - } - - public Uri getContentUri() { - return mContentUri; - } - - public abstract CharSequence getTypeLabel(Resources res, int type, CharSequence label); - } -} diff --git a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java b/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java deleted file mode 100644 index f6f662d..0000000 --- a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * 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; - -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.provider.ContactsContract.Contacts; -import android.text.TextUtils; -import android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; - -import com.android.ex.chips.BaseRecipientAdapter.DirectoryListQuery; -import com.android.ex.chips.BaseRecipientAdapter.DirectorySearchParams; -import com.android.ex.chips.DropdownChipLayouter.AdapterType; -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 - * queried by email or by phone number. - */ -public class RecipientAlternatesAdapter extends CursorAdapter { - static final int MAX_LOOKUPS = 50; - - private final long mCurrentId; - - private int mCheckedItemPosition = -1; - - private OnCheckedItemChangedListener mCheckedItemChangedListener; - - private static final String TAG = "RecipAlternates"; - - public static final int QUERY_TYPE_EMAIL = 0; - public static final int QUERY_TYPE_PHONE = 1; - private final Long mDirectoryId; - private DropdownChipLayouter mDropdownChipLayouter; - - private static final Map<String, String> sCorrectedPhotoUris = new HashMap<String, String>(); - - 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, BaseRecipientAdapter adapter, - ArrayList<String> inAddresses, Account account, RecipientMatchCallback callback) { - getMatchingRecipients(context, adapter, inAddresses, QUERY_TYPE_EMAIL, account, callback); - } - - /** - * Get a HashMap of address to RecipientEntry that contains all contact - * information for a contact with the provided address, if one exists. This - * may block the UI, so run it in an async task. - * - * @param context Context. - * @param inAddresses Array of addresses on which to perform the lookup. - * @param callback RecipientMatchCallback called when a match or matches are found. - * @return HashMap<String,RecipientEntry> - */ - public static void getMatchingRecipients(Context context, BaseRecipientAdapter adapter, - ArrayList<String> inAddresses, int addressType, Account account, - RecipientMatchCallback callback) { - Queries.Query query; - if (addressType == QUERY_TYPE_EMAIL) { - query = Queries.EMAIL; - } else { - query = Queries.PHONE; - } - int addressesSize = Math.min(MAX_LOOKUPS, inAddresses.size()); - 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.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i)); - bindString.append("?"); - if (i < addressesSize - 1) { - bindString.append(","); - } - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Doing reverse lookup for " + addresses.toString()); - } - - 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, null /* directoryId */); - 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 { - directoryCursor = context.getContentResolver().query(DirectoryListQuery.URI, - DirectoryListQuery.PROJECTION, null, null, null); - if (directoryCursor == null) { - paramsList = null; - } else { - 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); - - if (paramsList != null) { - Cursor directoryContactsCursor = null; - for (String unresolvedAddress : unresolvedAddresses) { - Long directoryId = null; - 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 { - directoryId = paramsList.get(i).directoryId; - break; - } - } - } - if (directoryContactsCursor != null) { - try { - final Map<String, RecipientEntry> entries = - processContactEntries(directoryContactsCursor, directoryId); - - for (final String address : entries.keySet()) { - matchesNotFound.remove(address); - } - - callback.matchesFound(entries); - } finally { - directoryContactsCursor.close(); - } - } - } - } - } - - // If no matches found in contact provider or the directories, try the extension - // matcher. - // todo (aalbert): This whole method needs to be in the adapter? - if (adapter != null) { - final Map<String, RecipientEntry> entries = - adapter.getMatchingRecipients(matchesNotFound); - if (entries != null && entries.size() > 0) { - callback.matchesFound(entries); - for (final String address : entries.keySet()) { - matchesNotFound.remove(address); - } - } - } - callback.matchesNotFound(matchesNotFound); - } - - private static HashMap<String, RecipientEntry> processContactEntries(Cursor c, - Long directoryId) { - 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), - directoryId, - c.getLong(Queries.Query.DATA_ID), - c.getString(Queries.Query.PHOTO_THUMBNAIL_URI), - true, - c.getString(Queries.Query.LOOKUP_KEY)); - - /* - * 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; - } - - /** - * 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 directoryId, - String lookupKey, long currentId, int queryMode, OnCheckedItemChangedListener listener, - DropdownChipLayouter dropdownChipLayouter) { - super(context, - getCursorForConstruction(context, contactId, directoryId, lookupKey, queryMode), 0); - mCurrentId = currentId; - mDirectoryId = directoryId; - mCheckedItemChangedListener = listener; - - mDropdownChipLayouter = dropdownChipLayouter; - } - - private static Cursor getCursorForConstruction(Context context, long contactId, - Long directoryId, String lookupKey, int queryType) { - final Cursor cursor; - final String desiredMimeType; - if (queryType == QUERY_TYPE_EMAIL) { - final Uri uri; - final StringBuilder selection = new StringBuilder(); - selection.append(Queries.EMAIL.getProjection()[Queries.Query.CONTACT_ID]); - selection.append(" = ?"); - - if (directoryId == null || lookupKey == null) { - uri = Queries.EMAIL.getContentUri(); - desiredMimeType = null; - } else { - final Uri.Builder builder = Contacts.getLookupUri(contactId, lookupKey).buildUpon(); - builder.appendPath(Contacts.Entity.CONTENT_DIRECTORY) - .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, - String.valueOf(directoryId)); - uri = builder.build(); - desiredMimeType = ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE; - } - cursor = context.getContentResolver().query( - uri, - Queries.EMAIL.getProjection(), - selection.toString(), new String[] { - String.valueOf(contactId) - }, null); - } else { - final Uri uri; - final StringBuilder selection = new StringBuilder(); - selection.append(Queries.PHONE.getProjection()[Queries.Query.CONTACT_ID]); - selection.append(" = ?"); - - if (lookupKey == null) { - uri = Queries.PHONE.getContentUri(); - desiredMimeType = null; - } else { - final Uri.Builder builder = Contacts.getLookupUri(contactId, lookupKey).buildUpon(); - builder.appendPath(Contacts.Entity.CONTENT_DIRECTORY) - .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, - String.valueOf(directoryId)); - uri = builder.build(); - desiredMimeType = ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE; - } - cursor = context.getContentResolver().query( - uri, - Queries.PHONE.getProjection(), - selection.toString(), new String[] { - String.valueOf(contactId) - }, null); - } - - final Cursor resultCursor = removeUndesiredDestinations(cursor, desiredMimeType, lookupKey); - cursor.close(); - - return resultCursor; - } - - /** - * @return a new cursor based on the given cursor with all duplicate destinations removed. - * - * It's only intended to use for the alternate list, so... - * - This method ignores all other fields and dedupe solely on the destination. Normally, - * if a cursor contains multiple contacts and they have the same destination, we'd still want - * to show both. - * - This method creates a MatrixCursor, so all data will be kept in memory. We wouldn't want - * to do this if the original cursor is large, but it's okay here because the alternate list - * won't be that big. - * - * @param desiredMimeType If this is non-<code>null</code>, only entries with this mime type - * will be added to the cursor - * @param lookupKey The lookup key used for this contact if there isn't one in the cursor. This - * should be the same one used in the query that returned the cursor - */ - // Visible for testing - static Cursor removeUndesiredDestinations(final Cursor original, final String desiredMimeType, - final String lookupKey) { - final MatrixCursor result = new MatrixCursor( - original.getColumnNames(), original.getCount()); - final HashSet<String> destinationsSeen = new HashSet<String>(); - - String defaultDisplayName = null; - String defaultPhotoThumbnailUri = null; - int defaultDisplayNameSource = 0; - - // Find some nice defaults in case we need them - original.moveToPosition(-1); - while (original.moveToNext()) { - final String mimeType = original.getString(Query.MIME_TYPE); - - if (ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals( - mimeType)) { - // Store this data - defaultDisplayName = original.getString(Query.NAME); - defaultPhotoThumbnailUri = original.getString(Query.PHOTO_THUMBNAIL_URI); - defaultDisplayNameSource = original.getInt(Query.DISPLAY_NAME_SOURCE); - break; - } - } - - original.moveToPosition(-1); - while (original.moveToNext()) { - if (desiredMimeType != null) { - final String mimeType = original.getString(Query.MIME_TYPE); - if (!desiredMimeType.equals(mimeType)) { - continue; - } - } - final String destination = original.getString(Query.DESTINATION); - if (destinationsSeen.contains(destination)) { - continue; - } - destinationsSeen.add(destination); - - final Object[] row = new Object[] { - original.getString(Query.NAME), - original.getString(Query.DESTINATION), - original.getInt(Query.DESTINATION_TYPE), - original.getString(Query.DESTINATION_LABEL), - original.getLong(Query.CONTACT_ID), - original.getLong(Query.DATA_ID), - original.getString(Query.PHOTO_THUMBNAIL_URI), - original.getInt(Query.DISPLAY_NAME_SOURCE), - original.getString(Query.LOOKUP_KEY), - original.getString(Query.MIME_TYPE) - }; - - if (row[Query.NAME] == null) { - row[Query.NAME] = defaultDisplayName; - } - if (row[Query.PHOTO_THUMBNAIL_URI] == null) { - row[Query.PHOTO_THUMBNAIL_URI] = defaultPhotoThumbnailUri; - } - if ((Integer) row[Query.DISPLAY_NAME_SOURCE] == 0) { - row[Query.DISPLAY_NAME_SOURCE] = defaultDisplayNameSource; - } - if (row[Query.LOOKUP_KEY] == null) { - row[Query.LOOKUP_KEY] = lookupKey; - } - - // Ensure we don't have two '?' like content://.../...?account_name=...?sz=... - final String photoThumbnailUri = (String) row[Query.PHOTO_THUMBNAIL_URI]; - if (photoThumbnailUri != null) { - if (sCorrectedPhotoUris.containsKey(photoThumbnailUri)) { - row[Query.PHOTO_THUMBNAIL_URI] = sCorrectedPhotoUris.get(photoThumbnailUri); - } else if (photoThumbnailUri.indexOf('?') != photoThumbnailUri.lastIndexOf('?')) { - final String[] parts = photoThumbnailUri.split("\\?"); - final StringBuilder correctedUriBuilder = new StringBuilder(); - for (int i = 0; i < parts.length; i++) { - if (i == 1) { - correctedUriBuilder.append("?"); // We only want one of these - } else if (i > 1) { - correctedUriBuilder.append("&"); // And we want these elsewhere - } - correctedUriBuilder.append(parts[i]); - } - - final String correctedUri = correctedUriBuilder.toString(); - sCorrectedPhotoUris.put(photoThumbnailUri, correctedUri); - row[Query.PHOTO_THUMBNAIL_URI] = correctedUri; - } - } - - result.addRow(row); - } - - return result; - } - - @Override - public long getItemId(int position) { - Cursor c = getCursor(); - if (c.moveToPosition(position)) { - c.getLong(Queries.Query.DATA_ID); - } - return -1; - } - - public RecipientEntry getRecipientEntry(int position) { - Cursor c = getCursor(); - c.moveToPosition(position); - return 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), - mDirectoryId, - c.getLong(Queries.Query.DATA_ID), - c.getString(Queries.Query.PHOTO_THUMBNAIL_URI), - true, - c.getString(Queries.Query.LOOKUP_KEY)); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - Cursor cursor = getCursor(); - cursor.moveToPosition(position); - if (convertView == null) { - convertView = mDropdownChipLayouter.newView(); - } - if (cursor.getLong(Queries.Query.DATA_ID) == mCurrentId) { - mCheckedItemPosition = position; - if (mCheckedItemChangedListener != null) { - mCheckedItemChangedListener.onCheckedItemChanged(mCheckedItemPosition); - } - } - bindView(convertView, convertView.getContext(), cursor); - return convertView; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - int position = cursor.getPosition(); - RecipientEntry entry = getRecipientEntry(position); - - mDropdownChipLayouter.bindView(view, null, entry, position, - AdapterType.RECIPIENT_ALTERNATES, null); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mDropdownChipLayouter.newView(); - } - - /*package*/ static interface OnCheckedItemChangedListener { - public void onCheckedItemChanged(int position); - } -} diff --git a/chips/src/com/android/ex/chips/RecipientEditTextView.java b/chips/src/com/android/ex/chips/RecipientEditTextView.java deleted file mode 100644 index 4339b9e..0000000 --- a/chips/src/com/android/ex/chips/RecipientEditTextView.java +++ /dev/null @@ -1,2988 +0,0 @@ -/* - - * 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; - -import android.app.Dialog; -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnDismissListener; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.Parcelable; -import android.text.Editable; -import android.text.InputType; -import android.text.Layout; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.text.method.QwertyKeyListener; -import android.text.style.ImageSpan; -import android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.ActionMode; -import android.view.ActionMode.Callback; -import android.view.DragEvent; -import android.view.GestureDetector; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewParent; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.Button; -import android.widget.Filterable; -import android.widget.ListAdapter; -import android.widget.ListPopupWindow; -import android.widget.ListView; -import android.widget.MultiAutoCompleteTextView; -import android.widget.ScrollView; -import android.widget.TextView; - -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; - -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; - -/** - * RecipientEditTextView is an auto complete text view for use with applications - * that use the new Chips UI for addressing a message to recipients. - */ -public class RecipientEditTextView extends MultiAutoCompleteTextView implements - OnItemClickListener, Callback, RecipientAlternatesAdapter.OnCheckedItemChangedListener, - GestureDetector.OnGestureListener, OnDismissListener, OnClickListener, - TextView.OnEditorActionListener { - - private static final char COMMIT_CHAR_COMMA = ','; - - 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(); - - private static final long DISMISS_DELAY = 300; - - // TODO: get correct number/ algorithm from with UX. - // Visible for testing. - /*package*/ static final int CHIP_LIMIT = 2; - - private static final int MAX_CHIPS_PARSED = 50; - - private static int sSelectedTextColor = -1; - - // Resources for displaying chips. - private Drawable mChipBackground = null; - - private Drawable mChipDelete = null; - - private Drawable mInvalidChipBackground; - - private Drawable mChipBackgroundPressed; - - private float mChipHeight; - - private float mChipFontSize; - - private float mLineSpacingExtra; - - private int mChipPadding; - - /** - * Enumerator for avatar position. See attr.xml for more details. - * 0 for end, 1 for start. - */ - private int mAvatarPosition; - - private static final int AVATAR_POSITION_END = 0; - - private static final int AVATAR_POSITION_START = 1; - - /** - * Enumerator for image span alignment. See attr.xml for more details. - * 0 for bottom, 1 for baseline. - */ - private int mImageSpanAlignment; - - private static final int IMAGE_SPAN_ALIGNMENT_BOTTOM = 0; - - private static final int IMAGE_SPAN_ALIGNMENT_BASELINE = 1; - - - private boolean mDisableDelete; - - private Tokenizer mTokenizer; - - private Validator mValidator; - - private DrawableRecipientChip mSelectedChip; - - private Bitmap mDefaultContactPhoto; - - private ImageSpan mMoreChip; - - private TextView mMoreItem; - - // VisibleForTesting - final ArrayList<String> mPendingChips = new ArrayList<String>(); - - private Handler mHandler; - - private int mPendingChipsCount = 0; - - private boolean mNoChips = false; - - private ListPopupWindow mAlternatesPopup; - - private ListPopupWindow mAddressPopup; - - // VisibleForTesting - ArrayList<DrawableRecipientChip> mTemporaryRecipients; - - private ArrayList<DrawableRecipientChip> mRemovedSpans; - - private boolean mShouldShrink = true; - - // Chip copy fields. - private GestureDetector mGestureDetector; - - private Dialog mCopyDialog; - - private String mCopyAddress; - - /** - * Used with {@link #mAlternatesPopup}. Handles clicks to alternate addresses for a - * selected chip. - */ - private OnItemClickListener mAlternatesListener; - - private int mCheckedItem; - - private TextWatcher mTextWatcher; - - // Obtain the enclosing scroll view, if it exists, so that the view can be - // scrolled to show the last line of chips content. - private ScrollView mScrollView; - - private boolean mTriedGettingScrollView; - - private boolean mDragEnabled = false; - - // This pattern comes from android.util.Patterns. It has been tweaked to handle a "1" before - // parens, so numbers such as "1 (425) 222-2342" match. - private static final Pattern PHONE_PATTERN - = Pattern.compile( // sdd = space, dot, or dash - "(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>* - + "(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() { - if (mTextWatcher == null) { - mTextWatcher = new RecipientTextWatcher(); - addTextChangedListener(mTextWatcher); - } - } - }; - - private IndividualReplacementTask mIndividualReplacements; - - private Runnable mHandlePendingChips = new Runnable() { - - @Override - public void run() { - handlePendingChips(); - } - - }; - - private Runnable mDelayedShrink = new Runnable() { - - @Override - public void run() { - shrink(); - } - - }; - - private int mMaxLines; - - private static int sExcessTopPadding = -1; - - private int mActionBarHeight; - - private boolean mAttachedToWindow; - - private DropdownChipLayouter mDropdownChipLayouter; - - public RecipientEditTextView(Context context, AttributeSet attrs) { - super(context, attrs); - setChipDimensions(context, attrs); - if (sSelectedTextColor == -1) { - sSelectedTextColor = context.getResources().getColor(android.R.color.white); - } - mAlternatesPopup = new ListPopupWindow(context); - mAddressPopup = new ListPopupWindow(context); - mCopyDialog = new Dialog(context); - mAlternatesListener = new OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> adapterView,View view, int position, - long rowId) { - mAlternatesPopup.setOnItemClickListener(null); - replaceChip(mSelectedChip, ((RecipientAlternatesAdapter) adapterView.getAdapter()) - .getRecipientEntry(position)); - Message delayed = Message.obtain(mHandler, DISMISS); - delayed.obj = mAlternatesPopup; - mHandler.sendMessageDelayed(delayed, DISMISS_DELAY); - clearComposingText(); - } - }; - setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - setOnItemClickListener(this); - setCustomSelectionActionModeCallback(this); - mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - if (msg.what == DISMISS) { - ((ListPopupWindow) msg.obj).dismiss(); - return; - } - super.handleMessage(msg); - } - }; - mTextWatcher = new RecipientTextWatcher(); - addTextChangedListener(mTextWatcher); - mGestureDetector = new GestureDetector(context, this); - setOnEditorActionListener(this); - - setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context)); - } - - protected void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) { - mDropdownChipLayouter = dropdownChipLayouter; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mAttachedToWindow = false; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mAttachedToWindow = true; - } - - @Override - public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) { - if (action == EditorInfo.IME_ACTION_DONE) { - if (commitDefault()) { - return true; - } - if (mSelectedChip != null) { - clearSelectedChip(); - return true; - } else if (focusNext()) { - return true; - } - } - return false; - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - InputConnection connection = super.onCreateInputConnection(outAttrs); - int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION; - if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) { - // clear the existing action - outAttrs.imeOptions ^= imeActions; - // set the DONE action - outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE; - } - 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*/ DrawableRecipientChip getLastChip() { - DrawableRecipientChip last = null; - DrawableRecipientChip[] chips = getSortedRecipients(); - if (chips != null && chips.length > 0) { - last = chips[chips.length - 1]; - } - return last; - } - - @Override - 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. - 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())); - } - super.onSelectionChanged(start, end); - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (!TextUtils.isEmpty(getText())) { - super.onRestoreInstanceState(null); - } else { - super.onRestoreInstanceState(state); - } - } - - @Override - public Parcelable onSaveInstanceState() { - // If the user changes orientation while they are editing, just roll back the selection. - clearSelectedChip(); - return super.onSaveInstanceState(); - } - - /** - * Convenience method: Append the specified text slice to the TextView's - * display buffer, upgrading it to BufferType.EDITABLE if it was - * not already editable. Commas are excluded as they are added automatically - * by the view. - */ - @Override - public void append(CharSequence text, int start, int end) { - // We don't care about watching text changes while appending. - if (mTextWatcher != null) { - removeTextChangedListener(mTextWatcher); - } - super.append(text, start, end); - if (!TextUtils.isEmpty(text) && TextUtils.getTrimmedLength(text) > 0) { - String displayString = text.toString(); - - 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(displayString); - } - } - // Put a message on the queue to make sure we ALWAYS handle pending - // chips. - if (mPendingChipsCount > 0) { - postHandlePendingChips(); - } - mHandler.post(mAddTextWatcher); - } - - @Override - public void onFocusChanged(boolean hasFocus, int direction, Rect previous) { - super.onFocusChanged(hasFocus, direction, previous); - if (!hasFocus) { - shrink(); - } else { - expand(); - } - } - - private int getExcessTopPadding() { - if (sExcessTopPadding == -1) { - sExcessTopPadding = (int) (mChipHeight + mLineSpacingExtra); - } - return sExcessTopPadding; - } - - @Override - public <T extends ListAdapter & Filterable> void setAdapter(T adapter) { - super.setAdapter(adapter); - BaseRecipientAdapter baseAdapter = (BaseRecipientAdapter) adapter; - baseAdapter.registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() { - @Override - public void onChanged(List<RecipientEntry> entries) { - // Scroll the chips field to the top of the screen so - // that the user can see as many results as possible. - if (entries != null && entries.size() > 0) { - scrollBottomIntoView(); - } - } - }); - baseAdapter.setDropdownChipLayouter(mDropdownChipLayouter); - } - - protected void scrollBottomIntoView() { - if (mScrollView != null && mShouldShrink) { - int[] location = new int[2]; - getLocationOnScreen(location); - int height = getHeight(); - int currentPos = location[1] + height; - // Desired position shows at least 1 line of chips below the action - // bar. We add excess padding to make sure this is always below other - // content. - int desiredPos = (int) mChipHeight + mActionBarHeight + getExcessTopPadding(); - if (currentPos > desiredPos) { - mScrollView.scrollBy(0, currentPos - desiredPos); - } - } - } - - protected ScrollView getScrollView() { - return mScrollView; - } - - @Override - public void performValidation() { - // Do nothing. Chips handles its own validation. - } - - private void shrink() { - if (mTokenizer == null) { - return; - } - long contactId = mSelectedChip != null ? mSelectedChip.getEntry().getContactId() : -1; - if (mSelectedChip != null && contactId != RecipientEntry.INVALID_CONTACT - && (!isPhoneQuery() && contactId != RecipientEntry.GENERATED_CONTACT)) { - clearSelectedChip(); - } else { - if (getWidth() <= 0) { - // We don't have the width yet which means the view hasn't been drawn yet - // and there is no reason to attempt to commit chips yet. - // This focus lost must be the result of an orientation change - // or an initial rendering. - // Re-post the shrink for later. - mHandler.removeCallbacks(mDelayedShrink); - mHandler.post(mDelayedShrink); - return; - } - // Reset any pending chips as they would have been handled - // when the field lost focus. - if (mPendingChipsCount > 0) { - postHandlePendingChips(); - } else { - Editable editable = getText(); - int end = getSelectionEnd(); - int start = mTokenizer.findTokenStart(editable, end); - DrawableRecipientChip[] chips = - getSpannable().getSpans(start, end, DrawableRecipientChip.class); - if ((chips == null || chips.length == 0)) { - Editable text = getText(); - int whatEnd = mTokenizer.findTokenEnd(text, start); - // This token was already tokenized, so skip past the ending token. - if (whatEnd < text.length() && text.charAt(whatEnd) == ',') { - whatEnd = movePastTerminators(whatEnd); - } - // In the middle of chip; treat this as an edit - // and commit the whole token. - int selEnd = getSelectionEnd(); - if (whatEnd != selEnd) { - handleEdit(start, whatEnd); - } else { - commitChip(start, end, editable); - } - } - } - mHandler.post(mAddTextWatcher); - } - createMoreChip(); - } - - private void expand() { - if (mShouldShrink) { - setMaxLines(Integer.MAX_VALUE); - } - removeMoreChip(); - setCursorVisible(true); - Editable text = getText(); - setSelection(text != null && text.length() > 0 ? text.length() : 0); - // If there are any temporary chips, try replacing them now that the user - // has expanded the field. - if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0) { - new RecipientReplacementTask().execute(); - mTemporaryRecipients = null; - } - } - - private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) { - paint.setTextSize(mChipFontSize); - if (maxWidth <= 0 && Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Max width is negative: " + maxWidth); - } - return TextUtils.ellipsize(text, paint, maxWidth, - TextUtils.TruncateAt.END); - } - - /** - * Creates a bitmap of the given contact on a selected chip. - * - * @param contact The recipient entry to pull data from. - * @param paint The paint to use to draw the bitmap. - */ - private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint) { - paint.setColor(sSelectedTextColor); - Bitmap photo; - if (mDisableDelete) { - // Show the avatar instead if we don't want to delete - photo = getAvatarIcon(contact); - } else { - photo = ((BitmapDrawable) mChipDelete).getBitmap(); - } - return createChipBitmap(contact, paint, photo, mChipBackgroundPressed); - } - - /** - * Creates a bitmap of the given contact on a selected chip. - * - * @param contact The recipient entry to pull data from. - * @param paint The paint to use to draw the bitmap. - */ - // TODO: Is leaveBlankIconSpacer obsolete now that we have left and right attributes? - private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint, - boolean leaveBlankIconSpacer) { - Drawable background = getChipBackground(contact); - Bitmap photo = getAvatarIcon(contact); - paint.setColor(getContext().getResources().getColor(android.R.color.black)); - return createChipBitmap(contact, paint, photo, background); - } - - private Bitmap createChipBitmap(RecipientEntry contact, TextPaint paint, Bitmap icon, - Drawable background) { - if (background == null) { - Log.w(TAG, "Unable to draw a background for the chips as it was never set"); - return Bitmap.createBitmap( - (int) mChipHeight * 2, (int) mChipHeight, Bitmap.Config.ARGB_8888); - } - - Rect backgroundPadding = new Rect(); - background.getPadding(backgroundPadding); - - // 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. - int height = (int) mChipHeight; - // Since the icon is a square, it's width is equal to the maximum height it can be inside - // the chip. - int iconWidth = height - backgroundPadding.top - backgroundPadding.bottom; - float[] widths = new float[1]; - paint.getTextWidths(" ", widths); - CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint, - calculateAvailableWidth() - iconWidth - widths[0] - backgroundPadding.left - - backgroundPadding.right);; - int textWidth = (int) paint.measureText(ellipsizedText, 0, ellipsizedText.length()); - - // Make sure there is a minimum chip width so the user can ALWAYS - // tap a chip without difficulty. - int width = Math.max(iconWidth * 2, textWidth + (mChipPadding * 2) + iconWidth - + backgroundPadding.left + backgroundPadding.right); - - // Create the background of the chip. - Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(tmpBitmap); - - // Draw the background drawable - background.setBounds(0, 0, width, height); - background.draw(canvas); - // Draw the text vertically aligned - int textX = shouldPositionAvatarOnRight() ? - mChipPadding + backgroundPadding.left : - width - backgroundPadding.right - mChipPadding - textWidth; - canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), - textX, getTextYOffset(ellipsizedText.toString(), paint, height), paint); - if (icon != null) { - // Draw the icon - int iconX = shouldPositionAvatarOnRight() ? - width - backgroundPadding.right - iconWidth : - backgroundPadding.left; - RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight()); - RectF dst = new RectF(iconX, - 0 + backgroundPadding.top, - iconX + iconWidth, - height - backgroundPadding.bottom); - drawIconOnCanvas(icon, canvas, paint, src, dst); - } - return tmpBitmap; - } - - /** - * Returns true if the avatar should be positioned at the right edge of the chip. - * Takes into account both the set avatar position (start or end) as well as whether - * the layout direction is LTR or RTL. - */ - private boolean shouldPositionAvatarOnRight() { - final boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ? - getLayoutDirection() == LAYOUT_DIRECTION_RTL : false; - final boolean assignedPosition = mAvatarPosition == AVATAR_POSITION_END; - // If in Rtl mode, the position should be flipped. - return isRtl ? !assignedPosition : assignedPosition; - } - - /** - * Returns the avatar icon to use for this recipient entry. Returns null if we don't want to - * draw an icon for this recipient. - */ - private Bitmap getAvatarIcon(RecipientEntry contact) { - // Don't draw photos for recipients that have been typed in OR generated on the fly. - long contactId = contact.getContactId(); - boolean drawPhotos = isPhoneQuery() ? - contactId != RecipientEntry.INVALID_CONTACT - : (contactId != RecipientEntry.INVALID_CONTACT - && (contactId != RecipientEntry.GENERATED_CONTACT && - !TextUtils.isEmpty(contact.getDisplayName()))); - - if (drawPhotos) { - byte[] photoBytes = contact.getPhotoBytes(); - // There may not be a photo yet if anything but the first contact address - // was selected. - if (photoBytes == null && contact.getPhotoThumbnailUri() != null) { - // TODO: cache this in the recipient entry? - getAdapter().fetchPhoto(contact, contact.getPhotoThumbnailUri()); - photoBytes = contact.getPhotoBytes(); - } - if (photoBytes != null) { - return BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length); - } else { - // TODO: can the scaled down default photo be cached? - return mDefaultContactPhoto; - } - } - - return null; - } - - /** - * Get the background drawable for a RecipientChip. - */ - // Visible for testing. - /* package */Drawable getChipBackground(RecipientEntry contact) { - return contact.isValid() ? mChipBackground : mInvalidChipBackground; - } - - /** - * Given a height, returns a Y offset that will draw the text in the middle of the height. - */ - protected 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(); - } - - /** - * Draws the icon onto the canvas given the source rectangle of the bitmap and the destination - * rectangle of the canvas. - */ - protected void drawIconOnCanvas(Bitmap icon, Canvas canvas, Paint paint, RectF src, RectF dst) { - Matrix matrix = new Matrix(); - matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL); - canvas.drawBitmap(icon, matrix, paint); - } - - 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."); - } - - TextPaint paint = getPaint(); - float defaultSize = paint.getTextSize(); - int defaultColor = paint.getColor(); - - Bitmap tmpBitmap; - if (pressed) { - tmpBitmap = createSelectedChip(contact, paint); - - } else { - 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()); - DrawableRecipientChip recipientChip = - new VisibleRecipientChip(result, contact, getImageSpanAlignment()); - // Return text to the original size. - paint.setTextSize(defaultSize); - paint.setColor(defaultColor); - return recipientChip; - } - - private int getImageSpanAlignment() { - switch (mImageSpanAlignment) { - case IMAGE_SPAN_ALIGNMENT_BASELINE: - return ImageSpan.ALIGN_BASELINE; - case IMAGE_SPAN_ALIGNMENT_BOTTOM: - return ImageSpan.ALIGN_BOTTOM; - default: - return ImageSpan.ALIGN_BOTTOM; - } - } - - /** - * Calculate the bottom of the line the chip will be located on using: - * 1) which line the chip appears on - * 2) the height of a chip - * 3) padding built into the edit text view - */ - private int calculateOffsetFromBottom(int line) { - // Line offsets start at zero. - int actualLine = getLineCount() - (line + 1); - return -((actualLine * ((int) mChipHeight) + getPaddingBottom()) + getPaddingTop()) - + getDropDownVerticalOffset(); - } - - /** - * Get the max amount of space a chip can take up. The formula takes into - * account the width of the EditTextView, any view padding, and padding - * that will be added to the chip. - */ - private float calculateAvailableWidth() { - return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2); - } - - - private void setChipDimensions(Context context, AttributeSet attrs) { - 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); - } - mChipBackgroundPressed = a - .getDrawable(R.styleable.RecipientEditTextView_chipBackgroundPressed); - if (mChipBackgroundPressed == null) { - mChipBackgroundPressed = r.getDrawable(R.drawable.chip_background_selected); - } - mChipDelete = a.getDrawable(R.styleable.RecipientEditTextView_chipDelete); - if (mChipDelete == null) { - mChipDelete = r.getDrawable(R.drawable.chip_delete); - } - mChipPadding = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipPadding, -1); - if (mChipPadding == -1) { - mChipPadding = (int) r.getDimension(R.dimen.chip_padding); - } - - mDefaultContactPhoto = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture); - - mMoreItem = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.more_item, null); - - mChipHeight = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipHeight, -1); - if (mChipHeight == -1) { - mChipHeight = r.getDimension(R.dimen.chip_height); - } - mChipFontSize = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipFontSize, -1); - if (mChipFontSize == -1) { - mChipFontSize = r.getDimension(R.dimen.chip_text_size); - } - mInvalidChipBackground = a - .getDrawable(R.styleable.RecipientEditTextView_invalidChipBackground); - if (mInvalidChipBackground == null) { - mInvalidChipBackground = r.getDrawable(R.drawable.chip_background_invalid); - } - mAvatarPosition = a.getInt(R.styleable.RecipientEditTextView_avatarPosition, 0); - mImageSpanAlignment = a.getInt(R.styleable.RecipientEditTextView_imageSpanAlignment, 0); - mDisableDelete = a.getBoolean(R.styleable.RecipientEditTextView_disableDelete, false); - - 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() - .getDisplayMetrics()); - } - - a.recycle(); - } - - // Visible for testing. - /* package */ void setMoreItem(TextView moreItem) { - mMoreItem = moreItem; - } - - - // Visible for testing. - /* package */ void setChipBackground(Drawable chipBackground) { - mChipBackground = chipBackground; - } - - // Visible for testing. - /* package */ void setChipHeight(int height) { - mChipHeight = height; - } - - public float getChipHeight() { - return mChipHeight; - } - - /** - * Set whether to shrink the recipients field such that at most - * one line of recipients chips are shown when the field loses - * focus. By default, the number of displayed recipients will be - * limited and a "more" chip will be shown when focus is lost. - * @param shrink - */ - public void setOnFocusListShrinkRecipients(boolean shrink) { - mShouldShrink = shrink; - } - - @Override - public void onSizeChanged(int width, int height, int oldw, int oldh) { - super.onSizeChanged(width, height, oldw, oldh); - if (width != 0 && height != 0) { - if (mPendingChipsCount > 0) { - postHandlePendingChips(); - } else { - checkChipWidths(); - } - } - // Try to find the scroll view parent, if it exists. - if (mScrollView == null && !mTriedGettingScrollView) { - ViewParent parent = getParent(); - while (parent != null && !(parent instanceof ScrollView)) { - parent = parent.getParent(); - } - if (parent != null) { - mScrollView = (ScrollView) parent; - } - mTriedGettingScrollView = true; - } - } - - private void postHandlePendingChips() { - mHandler.removeCallbacks(mHandlePendingChips); - mHandler.post(mHandlePendingChips); - } - - private void checkChipWidths() { - // Check the widths of the associated chips. - DrawableRecipientChip[] chips = getSortedRecipients(); - if (chips != null) { - Rect bounds; - for (DrawableRecipientChip chip : chips) { - bounds = chip.getBounds(); - if (getWidth() > 0 && bounds.right - bounds.left > - getWidth() - getPaddingLeft() - getPaddingRight()) { - // Need to redraw that chip. - replaceChip(chip, chip.getEntry()); - } - } - } - } - - // Visible for testing. - /*package*/ void handlePendingChips() { - if (getViewWidth() <= 0) { - // The widget has not been sized yet. - // This will be called as a result of onSizeChanged - // at a later point. - return; - } - if (mPendingChipsCount <= 0) { - return; - } - - synchronized (mPendingChips) { - Editable editable = getText(); - // Tokenize! - if (mPendingChipsCount <= MAX_CHIPS_PARSED) { - for (int i = 0; i < mPendingChips.size(); i++) { - String current = mPendingChips.get(i); - int tokenStart = editable.toString().indexOf(current); - // 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. - if (tokenEnd < editable.length() - 2 - && editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) { - tokenEnd++; - } - createReplacementChip(tokenStart, tokenEnd, editable, i < CHIP_LIMIT - || !mShouldShrink); - } - mPendingChipsCount--; - } - sanitizeEnd(); - } else { - mNoChips = true; - } - - if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0 - && mTemporaryRecipients.size() <= RecipientAlternatesAdapter.MAX_LOOKUPS) { - if (hasFocus() || mTemporaryRecipients.size() < CHIP_LIMIT) { - new RecipientReplacementTask().execute(); - mTemporaryRecipients = null; - } else { - // Create the "more" chip - mIndividualReplacements = new IndividualReplacementTask(); - mIndividualReplacements.execute(new ArrayList<DrawableRecipientChip>( - mTemporaryRecipients.subList(0, CHIP_LIMIT))); - if (mTemporaryRecipients.size() > CHIP_LIMIT) { - mTemporaryRecipients = new ArrayList<DrawableRecipientChip>( - mTemporaryRecipients.subList(CHIP_LIMIT, - mTemporaryRecipients.size())); - } else { - mTemporaryRecipients = null; - } - createMoreChip(); - } - } else { - // There are too many recipients to look up, so just fall back - // to showing addresses for all of them. - mTemporaryRecipients = null; - createMoreChip(); - } - mPendingChipsCount = 0; - mPendingChips.clear(); - } - } - - // Visible for testing. - /*package*/ int getViewWidth() { - return getWidth(); - } - - /** - * Remove any characters after the last valid chip. - */ - // Visible for testing. - /*package*/ void sanitizeEnd() { - // Don't sanitize while we are waiting for pending chips to complete. - if (mPendingChipsCount > 0) { - return; - } - // Find the last chip; eliminate any commit characters after it. - DrawableRecipientChip[] chips = getSortedRecipients(); - Spannable spannable = getSpannable(); - if (chips != null && chips.length > 0) { - int end; - mMoreChip = getMoreChip(); - if (mMoreChip != null) { - end = spannable.getSpanEnd(mMoreChip); - } else { - end = getSpannable().getSpanEnd(getLastChip()); - } - Editable editable = getText(); - int length = editable.length(); - if (length > end) { - // See what characters occur after that and eliminate them. - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "There were extra characters after the last tokenizable entry." - + editable); - } - editable.delete(end + 1, length); - } - } - } - - /** - * 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. - */ - // 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); - 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) { - DrawableRecipientChip chip = null; - try { - if (!mNoChips) { - /* - * leave space for the contact icon if this is not just an - * email address - */ - boolean leaveSpace = TextUtils.isEmpty(entry.getDisplayName()) - || TextUtils.equals(entry.getDisplayName(), - entry.getDestination()); - chip = visible ? - constructChipSpan(entry, false, leaveSpace) - : new InvisibleRecipientChip(entry); - } - } catch (NullPointerException e) { - Log.e(TAG, e.getMessage(), e); - } - editable.setSpan(chip, tokenStart, tokenEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - // Add this chip to the list of entries "to replace" - if (chip != null) { - if (mTemporaryRecipients == null) { - mTemporaryRecipients = new ArrayList<DrawableRecipientChip>(); - } - chip.setOriginalText(token); - mTemporaryRecipients.add(chip); - } - } - } - - private static boolean isPhoneNumber(String number) { - // TODO: replace this function with libphonenumber's isPossibleNumber (see - // PhoneNumberUtil). One complication is that it requires the sender's region which - // comes from the CurrentCountryIso. For now, let's just do this simple match. - if (TextUtils.isEmpty(number)) { - return false; - } - - Matcher match = PHONE_PATTERN.matcher(number); - return match.matches(); - } - - // VisibleForTesting - RecipientEntry createTokenizedEntry(final String token) { - if (TextUtils.isEmpty(token)) { - return null; - } - if (isPhoneQuery() && isPhoneNumber(token)) { - return RecipientEntry.constructFakePhoneEntry(token, true); - } - Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token); - String display = null; - 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)) { - return RecipientEntry.constructGeneratedEntry(display, tokens[0].getAddress(), - isValid); - } else { - display = tokens[0].getAddress(); - if (!TextUtils.isEmpty(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 && !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, - // 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 - // 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, isValid); - } - - private boolean isValid(String text) { - return mValidator == null ? true : mValidator.isValid(text); - } - - private static String tokenizeAddress(String destination) { - Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination); - if (tokens != null && tokens.length > 0) { - return tokens[0].getAddress(); - } - return destination; - } - - @Override - public void setTokenizer(Tokenizer tokenizer) { - mTokenizer = tokenizer; - super.setTokenizer(mTokenizer); - } - - @Override - public void setValidator(Validator validator) { - mValidator = validator; - super.setValidator(validator); - } - - /** - * We cannot use the default mechanism for replaceText. Instead, - * we override onItemClickListener so we can get all the associated - * contact information including display text, address, and id. - */ - @Override - protected void replaceText(CharSequence text) { - return; - } - - /** - * Dismiss any selected chips when the back key is pressed. - */ - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && mSelectedChip != null) { - clearSelectedChip(); - return true; - } - return super.onKeyPreIme(keyCode, event); - } - - /** - * Monitor key presses in this view to see if the user types - * any commit keys, which consist of ENTER, TAB, or DPAD_CENTER. - * If the user has entered text that has contact matches and types - * a commit key, create a chip from the topmost matching contact. - * If the user has entered text that has no contact matches and types - * a commit key, then create a chip from the text they have entered. - */ - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_TAB: - if (event.hasNoModifiers()) { - if (mSelectedChip != null) { - clearSelectedChip(); - } else { - commitDefault(); - } - } - break; - } - return super.onKeyUp(keyCode, event); - } - - private boolean focusNext() { - View next = focusSearch(View.FOCUS_DOWN); - if (next != null) { - next.requestFocus(); - return true; - } - return false; - } - - /** - * Create a chip from the default selection. If the popup is showing, the - * default is the first item in the popup suggestions list. Otherwise, it is - * whatever the user had typed in. End represents where the the tokenizer - * should search for a token to turn into a chip. - * @return If a chip was created from a real contact. - */ - private boolean commitDefault() { - // If there is no tokenizer, don't try to commit. - if (mTokenizer == null) { - return false; - } - Editable editable = getText(); - int end = getSelectionEnd(); - int start = mTokenizer.findTokenStart(editable, end); - - if (shouldCreateChip(start, end)) { - int whatEnd = mTokenizer.findTokenEnd(getText(), start); - // In the middle of chip; treat this as an edit - // and commit the whole token. - whatEnd = movePastTerminators(whatEnd); - if (whatEnd != getSelectionEnd()) { - handleEdit(start, whatEnd); - return true; - } - return commitChip(start, end , editable); - } - return false; - } - - private void commitByCharacter() { - // We can't possibly commit by character if we can't tokenize. - if (mTokenizer == null) { - return; - } - Editable editable = getText(); - int end = getSelectionEnd(); - int start = mTokenizer.findTokenStart(editable, end); - if (shouldCreateChip(start, end)) { - commitChip(start, end, editable); - } - setSelection(getText().length()); - } - - private boolean commitChip(int start, int end, Editable editable) { - ListAdapter adapter = getAdapter(); - if (adapter != null && adapter.getCount() > 0 && enoughToFilter() - && end == getSelectionEnd() && !isPhoneQuery()) { - // choose the first entry. - submitItemAtPosition(0); - dismissDropDown(); - return true; - } else { - int tokenEnd = mTokenizer.findTokenEnd(editable, start); - if (editable.length() > tokenEnd + 1) { - char charAt = editable.charAt(tokenEnd + 1); - if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) { - tokenEnd++; - } - } - String text = editable.toString().substring(start, tokenEnd).trim(); - clearComposingText(); - if (text != null && text.length() > 0 && !text.equals(" ")) { - RecipientEntry entry = createTokenizedEntry(text); - if (entry != null) { - QwertyKeyListener.markAsReplaced(editable, start, end, ""); - CharSequence chipText = createChip(entry, false); - if (chipText != null && start > -1 && end > -1) { - editable.replace(start, end, chipText); - } - } - // Only dismiss the dropdown if it is related to the text we - // just committed. - // For paste, it may not be as there are possibly multiple - // tokens being added. - if (end == getSelectionEnd()) { - dismissDropDown(); - } - sanitizeBetween(); - return true; - } - } - return false; - } - - // Visible for testing. - /* package */ void sanitizeBetween() { - // Don't sanitize while we are waiting for content to chipify. - if (mPendingChipsCount > 0) { - return; - } - // Find the last chip. - DrawableRecipientChip[] recips = getSortedRecipients(); - if (recips != null && recips.length > 0) { - DrawableRecipientChip last = recips[recips.length - 1]; - DrawableRecipientChip beforeLast = null; - if (recips.length > 1) { - beforeLast = recips[recips.length - 2]; - } - int startLooking = 0; - int end = getSpannable().getSpanStart(last); - if (beforeLast != null) { - startLooking = getSpannable().getSpanEnd(beforeLast); - Editable text = getText(); - if (startLooking == -1 || startLooking > text.length() - 1) { - // There is nothing after this chip. - return; - } - if (text.charAt(startLooking) == ' ') { - startLooking++; - } - } - if (startLooking >= 0 && end >= 0 && startLooking < end) { - getText().delete(startLooking, end); - } - } - } - - private boolean shouldCreateChip(int start, int end) { - return !mNoChips && hasFocus() && enoughToFilter() && !alreadyHasChip(start, end); - } - - private boolean alreadyHasChip(int start, int end) { - if (mNoChips) { - return true; - } - DrawableRecipientChip[] chips = - getSpannable().getSpans(start, end, DrawableRecipientChip.class); - if ((chips == null || chips.length == 0)) { - return false; - } - return true; - } - - private void handleEdit(int start, int end) { - if (start == -1 || end == -1) { - // This chip no longer exists in the field. - dismissDropDown(); - return; - } - // This is in the middle of a chip, so select out the whole chip - // and commit it. - Editable editable = getText(); - setSelection(end); - String text = getText().toString().substring(start, end); - if (!TextUtils.isEmpty(text)) { - RecipientEntry entry = RecipientEntry.constructFakeEntry(text, isValid(text)); - QwertyKeyListener.markAsReplaced(editable, start, end, ""); - CharSequence chipText = createChip(entry, false); - int selEnd = getSelectionEnd(); - if (chipText != null && start > -1 && selEnd > -1) { - editable.replace(start, selEnd, chipText); - } - } - dismissDropDown(); - } - - /** - * If there is a selected chip, delegate the key events - * to the selected chip. - */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mSelectedChip != null && keyCode == KeyEvent.KEYCODE_DEL) { - if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) { - mAlternatesPopup.dismiss(); - } - removeChip(mSelectedChip); - } - - 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); - } - - // Visible for testing. - /* package */ Spannable getSpannable() { - return getText(); - } - - private int getChipStart(DrawableRecipientChip chip) { - return getSpannable().getSpanStart(chip); - } - - private int getChipEnd(DrawableRecipientChip chip) { - return getSpannable().getSpanEnd(chip); - } - - /** - * Instead of filtering on the entire contents of the edit box, - * this subclass method filters on the range from - * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} - * if the length of that range meets or exceeds {@link #getThreshold} - * and makes sure that the range is not already a Chip. - */ - @Override - protected void performFiltering(CharSequence text, int keyCode) { - boolean isCompletedToken = isCompletedToken(text); - if (enoughToFilter() && !isCompletedToken) { - int end = getSelectionEnd(); - int start = mTokenizer.findTokenStart(text, end); - // If this is a RecipientChip, don't filter - // on its contents. - Spannable span = getSpannable(); - DrawableRecipientChip[] chips = span.getSpans(start, end, DrawableRecipientChip.class); - if (chips != null && chips.length > 0) { - dismissDropDown(); - return; - } - } else if (isCompletedToken) { - dismissDropDown(); - return; - } - super.performFiltering(text, keyCode); - } - - // Visible for testing. - /*package*/ boolean isCompletedToken(CharSequence text) { - if (TextUtils.isEmpty(text)) { - return false; - } - // Check to see if this is a completed token before filtering. - int end = text.length(); - int start = mTokenizer.findTokenStart(text, end); - String token = text.toString().substring(start, end).trim(); - if (!TextUtils.isEmpty(token)) { - char atEnd = token.charAt(token.length() - 1); - return atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON; - } - return false; - } - - private void clearSelectedChip() { - if (mSelectedChip != null) { - unselectChip(mSelectedChip); - mSelectedChip = null; - } - setCursorVisible(true); - } - - /** - * Monitor touch events in the RecipientEditTextView. - * If the view does not have focus, any tap on the view - * will just focus the view. If the view has focus, determine - * if the touch target is a recipient chip. If it is and the chip - * is not selected, select it and clear any other selected chips. - * If it isn't, then select that chip. - */ - @Override - public boolean onTouchEvent(MotionEvent event) { - if (!isFocused()) { - // Ignore any chip taps until this view is focused. - return super.onTouchEvent(event); - } - boolean handled = super.onTouchEvent(event); - int action = event.getAction(); - boolean chipWasSelected = false; - if (mSelectedChip == null) { - mGestureDetector.onTouchEvent(event); - } - if (mCopyAddress == null && action == MotionEvent.ACTION_UP) { - float x = event.getX(); - float y = event.getY(); - int offset = putOffsetInRange(x, y); - DrawableRecipientChip currentChip = findChip(offset); - if (currentChip != null) { - if (action == MotionEvent.ACTION_UP) { - if (mSelectedChip != null && mSelectedChip != currentChip) { - clearSelectedChip(); - mSelectedChip = selectChip(currentChip); - } else if (mSelectedChip == null) { - setSelection(getText().length()); - commitDefault(); - mSelectedChip = selectChip(currentChip); - } else { - onClick(mSelectedChip, offset, x, y); - } - } - chipWasSelected = true; - handled = true; - } else if (mSelectedChip != null && shouldShowEditableText(mSelectedChip)) { - chipWasSelected = true; - } - } - if (action == MotionEvent.ACTION_UP && !chipWasSelected) { - clearSelectedChip(); - } - return handled; - } - - private void scrollLineIntoView(int line) { - if (mScrollView != null) { - mScrollView.smoothScrollBy(0, calculateOffsetFromBottom(line)); - } - } - - 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) { - if (!mAttachedToWindow) { - return; - } - 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(DrawableRecipientChip chip) { - return new RecipientAlternatesAdapter(getContext(), chip.getContactId(), - chip.getDirectoryId(), chip.getLookupKey(), chip.getDataId(), - getAdapter().getQueryType(), this, mDropdownChipLayouter); - } - - private ListAdapter createSingleAddressAdapter(DrawableRecipientChip currentChip) { - return new SingleRecipientArrayAdapter(getContext(), currentChip.getEntry(), - mDropdownChipLayouter); - } - - @Override - public void onCheckedItemChanged(int position) { - ListView listView = mAlternatesPopup.getListView(); - if (listView != null && listView.getCheckedItemCount() == 0) { - listView.setItemChecked(position, true); - } - mCheckedItem = position; - } - - private int putOffsetInRange(final float x, final float y) { - final int offset; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - offset = getOffsetForPosition(x, y); - } else { - offset = supportGetOffsetForPosition(x, y); - } - - return putOffsetInRange(offset); - } - - // TODO: This algorithm will need a lot of tweaking after more people have used - // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring - // what comes before the finger. - private int putOffsetInRange(int o) { - int offset = o; - Editable text = getText(); - int length = text.length(); - // Remove whitespace from end to find "real end" - int realLength = length; - for (int i = length - 1; i >= 0; i--) { - if (text.charAt(i) == ' ') { - realLength--; - } else { - break; - } - } - - // If the offset is beyond or at the end of the text, - // leave it alone. - if (offset >= realLength) { - return offset; - } - Editable editable = getText(); - while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) { - // Keep walking backward! - offset--; - } - return offset; - } - - private static int findText(Editable text, int offset) { - if (text.charAt(offset) != ' ') { - return offset; - } - return -1; - } - - 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++) { - DrawableRecipientChip chip = chips[i]; - int start = getChipStart(chip); - int end = getChipEnd(chip); - if (offset >= start && offset <= end) { - return chip; - } - } - return null; - } - - // Visible for testing. - // Use this method to generate text to add to the list of addresses. - /* package */String createAddressText(RecipientEntry entry) { - String display = entry.getDisplayName(); - String address = entry.getDestination(); - if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) { - display = null; - } - String trimmedDisplayText; - if (isPhoneQuery() && isPhoneNumber(address)) { - trimmedDisplayText = address.trim(); - } else { - if (address != null) { - // 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(); - } - } - Rfc822Token token = new Rfc822Token(display, address, null); - trimmedDisplayText = token.toString().trim(); - } - int index = trimmedDisplayText.indexOf(","); - return mTokenizer != null && !TextUtils.isEmpty(trimmedDisplayText) - && index < trimmedDisplayText.length() - 1 ? (String) mTokenizer - .terminateToken(trimmedDisplayText) : trimmedDisplayText; - } - - // Visible for testing. - // Use this method to generate text to display in a chip. - /*package*/ String createChipDisplayText(RecipientEntry entry) { - String display = entry.getDisplayName(); - String address = entry.getDestination(); - if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) { - display = null; - } - if (!TextUtils.isEmpty(display)) { - return display; - } else if (!TextUtils.isEmpty(address)){ - return address; - } else { - return new Rfc822Token(display, address, null).toString(); - } - } - - private CharSequence createChip(RecipientEntry entry, boolean pressed) { - String displayText = createAddressText(entry); - if (TextUtils.isEmpty(displayText)) { - return null; - } - SpannableString chipText = null; - // Always leave a blank space at the end of a chip. - int textLength = displayText.length() - 1; - chipText = new SpannableString(displayText); - if (!mNoChips) { - try { - DrawableRecipientChip chip = constructChipSpan(entry, pressed, - false /* leave space for contact icon */); - chipText.setSpan(chip, 0, textLength, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - chip.setOriginalText(chipText.toString()); - } catch (NullPointerException e) { - Log.e(TAG, e.getMessage(), e); - return null; - } - } - return chipText; - } - - /** - * When an item in the suggestions list has been clicked, create a chip from the - * contact information of the selected item. - */ - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - if (position < 0) { - return; - } - submitItemAtPosition(position); - } - - private void submitItemAtPosition(int position) { - RecipientEntry entry = createValidatedEntry(getAdapter().getItem(position)); - if (entry == null) { - return; - } - clearComposingText(); - - int end = getSelectionEnd(); - int start = mTokenizer.findTokenStart(getText(), end); - - Editable editable = getText(); - QwertyKeyListener.markAsReplaced(editable, start, end, ""); - CharSequence chip = createChip(entry, false); - if (chip != null && start >= 0 && end >= 0) { - editable.replace(start, end, chip); - } - sanitizeBetween(); - } - - private RecipientEntry createValidatedEntry(RecipientEntry item) { - if (item == null) { - return null; - } - final RecipientEntry entry; - // If the display name and the address are the same, or if this is a - // valid contact, but the destination is invalid, then make this a fake - // recipient that is editable. - String destination = item.getDestination(); - if (!isPhoneQuery() && item.getContactId() == RecipientEntry.GENERATED_CONTACT) { - entry = RecipientEntry.constructGeneratedEntry(item.getDisplayName(), - 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, item.isValid()); - } else { - entry = item; - } - return entry; - } - - /** Returns a collection of contact Id for each chip inside this View. */ - /* package */ Collection<Long> getContactIds() { - final Set<Long> result = new HashSet<Long>(); - DrawableRecipientChip[] chips = getSortedRecipients(); - if (chips != null) { - for (DrawableRecipientChip chip : chips) { - result.add(chip.getContactId()); - } - } - return result; - } - - - /** Returns a collection of data Id for each chip inside this View. May be null. */ - /* package */ Collection<Long> getDataIds() { - final Set<Long> result = new HashSet<Long>(); - DrawableRecipientChip [] chips = getSortedRecipients(); - if (chips != null) { - for (DrawableRecipientChip chip : chips) { - result.add(chip.getDataId()); - } - } - return result; - } - - // Visible for testing. - /* package */DrawableRecipientChip[] getSortedRecipients() { - DrawableRecipientChip[] recips = getSpannable() - .getSpans(0, getText().length(), DrawableRecipientChip.class); - ArrayList<DrawableRecipientChip> recipientsList = new ArrayList<DrawableRecipientChip>( - Arrays.asList(recips)); - final Spannable spannable = getSpannable(); - Collections.sort(recipientsList, new Comparator<DrawableRecipientChip>() { - - @Override - public int compare(DrawableRecipientChip first, DrawableRecipientChip second) { - int firstStart = spannable.getSpanStart(first); - int secondStart = spannable.getSpanStart(second); - if (firstStart < secondStart) { - return -1; - } else if (firstStart > secondStart) { - return 1; - } else { - return 0; - } - } - }); - return recipientsList.toArray(new DrawableRecipientChip[recipientsList.size()]); - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - /** - * No chips are selectable. - */ - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } - - // Visible for testing. - /* package */ImageSpan getMoreChip() { - MoreImageSpan[] moreSpans = getSpannable().getSpans(0, getText().length(), - MoreImageSpan.class); - return moreSpans != null && moreSpans.length > 0 ? moreSpans[0] : null; - } - - private MoreImageSpan createMoreSpan(int count) { - String moreText = String.format(mMoreItem.getText().toString(), count); - TextPaint morePaint = new TextPaint(getPaint()); - morePaint.setTextSize(mMoreItem.getTextSize()); - morePaint.setColor(mMoreItem.getCurrentTextColor()); - int width = (int)morePaint.measureText(moreText) + mMoreItem.getPaddingLeft() - + mMoreItem.getPaddingRight(); - int height = getLineHeight(); - Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(drawable); - int adjustedHeight = height; - Layout layout = getLayout(); - if (layout != null) { - adjustedHeight -= layout.getLineDescent(0); - } - canvas.drawText(moreText, 0, moreText.length(), 0, adjustedHeight, morePaint); - - Drawable result = new BitmapDrawable(getResources(), drawable); - result.setBounds(0, 0, width, height); - return new MoreImageSpan(result); - } - - // Visible for testing. - /*package*/ void createMoreChipPlainText() { - // Take the first <= CHIP_LIMIT addresses and get to the end of the second one. - Editable text = getText(); - int start = 0; - int end = start; - for (int i = 0; i < CHIP_LIMIT; i++) { - end = movePastTerminators(mTokenizer.findTokenEnd(text, start)); - start = end; // move to the next token and get its end. - } - // Now, count total addresses. - start = 0; - int tokenCount = countTokens(text); - MoreImageSpan moreSpan = createMoreSpan(tokenCount - CHIP_LIMIT); - SpannableString chipText = new SpannableString(text.subSequence(end, text.length())); - chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - text.replace(end, text.length(), chipText); - mMoreChip = moreSpan; - } - - // Visible for testing. - /* package */int countTokens(Editable text) { - int tokenCount = 0; - int start = 0; - while (start < text.length()) { - start = movePastTerminators(mTokenizer.findTokenEnd(text, start)); - tokenCount++; - if (start >= text.length()) { - break; - } - } - return tokenCount; - } - - /** - * Create the more chip. The more chip is text that replaces any chips that - * do not fit in the pre-defined available space when the - * RecipientEditTextView loses focus. - */ - // Visible for testing. - /* package */ void createMoreChip() { - if (mNoChips) { - createMoreChipPlainText(); - return; - } - - if (!mShouldShrink) { - return; - } - ImageSpan[] tempMore = getSpannable().getSpans(0, getText().length(), MoreImageSpan.class); - if (tempMore.length > 0) { - getSpannable().removeSpan(tempMore[0]); - } - DrawableRecipientChip[] recipients = getSortedRecipients(); - - if (recipients == null || recipients.length <= CHIP_LIMIT) { - mMoreChip = null; - return; - } - Spannable spannable = getSpannable(); - int numRecipients = recipients.length; - int overage = numRecipients - CHIP_LIMIT; - MoreImageSpan moreSpan = createMoreSpan(overage); - mRemovedSpans = new ArrayList<DrawableRecipientChip>(); - int totalReplaceStart = 0; - int totalReplaceEnd = 0; - Editable text = getText(); - for (int i = numRecipients - overage; i < recipients.length; i++) { - mRemovedSpans.add(recipients[i]); - if (i == numRecipients - overage) { - totalReplaceStart = spannable.getSpanStart(recipients[i]); - } - if (i == recipients.length - 1) { - totalReplaceEnd = spannable.getSpanEnd(recipients[i]); - } - if (mTemporaryRecipients == null || !mTemporaryRecipients.contains(recipients[i])) { - int spanStart = spannable.getSpanStart(recipients[i]); - int spanEnd = spannable.getSpanEnd(recipients[i]); - recipients[i].setOriginalText(text.toString().substring(spanStart, spanEnd)); - } - spannable.removeSpan(recipients[i]); - } - if (totalReplaceEnd < text.length()) { - totalReplaceEnd = text.length(); - } - int end = Math.max(totalReplaceStart, totalReplaceEnd); - int start = Math.min(totalReplaceStart, totalReplaceEnd); - SpannableString chipText = new SpannableString(text.subSequence(start, end)); - chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - text.replace(start, end, chipText); - mMoreChip = moreSpan; - // If adding the +more chip goes over the limit, resize accordingly. - if (!isPhoneQuery() && getLineCount() > mMaxLines) { - setMaxLines(getLineCount()); - } - } - - /** - * Replace the more chip, if it exists, with all of the recipient chips it had - * replaced when the RecipientEditTextView gains focus. - */ - // Visible for testing. - /*package*/ void removeMoreChip() { - if (mMoreChip != null) { - Spannable span = getSpannable(); - span.removeSpan(mMoreChip); - mMoreChip = null; - // Re-add the spans that were removed. - if (mRemovedSpans != null && mRemovedSpans.size() > 0) { - // Recreate each removed span. - DrawableRecipientChip[] recipients = getSortedRecipients(); - // Start the search for tokens after the last currently visible - // chip. - if (recipients == null || recipients.length == 0) { - return; - } - int end = span.getSpanEnd(recipients[recipients.length - 1]); - Editable editable = getText(); - for (DrawableRecipientChip chip : mRemovedSpans) { - int chipStart; - int chipEnd; - String token; - // Need to find the location of the chip, again. - token = (String) chip.getOriginalText(); - // As we find the matching recipient for the remove spans, - // reduce the size of the string we need to search. - // That way, if there are duplicates, we always find the correct - // recipient. - chipStart = editable.toString().indexOf(token, end); - end = chipEnd = Math.min(editable.length(), chipStart + token.length()); - // Only set the span if we found a matching token. - if (chipStart != -1) { - editable.setSpan(chip, chipStart, chipEnd, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - mRemovedSpans.clear(); - } - } - } - - /** - * Show specified chip as selected. If the RecipientChip is just an email address, - * selecting the chip will take the contents of the chip and place it at - * the end of the RecipientEditTextView for inline editing. If the - * RecipientChip is a complete contact, then selecting the chip - * will change the background color of the chip, show the delete icon, - * and a popup window with the address in use highlighted and any other - * alternate addresses for the contact. - * @param currentChip Chip to select. - * @return A RecipientChip in the selected state or null if the chip - * just contained an email address. - */ - private DrawableRecipientChip selectChip(DrawableRecipientChip currentChip) { - if (shouldShowEditableText(currentChip)) { - CharSequence text = currentChip.getValue(); - Editable editable = getText(); - Spannable spannable = getSpannable(); - int spanStart = spannable.getSpanStart(currentChip); - int spanEnd = spannable.getSpanEnd(currentChip); - spannable.removeSpan(currentChip); - editable.delete(spanStart, spanEnd); - setCursorVisible(true); - setSelection(editable.length()); - editable.append(text); - return constructChipSpan( - RecipientEntry.constructFakeEntry((String) text, isValid(text.toString())), - true, false); - } else if (currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT) { - int start = getChipStart(currentChip); - int end = getChipEnd(currentChip); - getSpannable().removeSpan(currentChip); - DrawableRecipientChip newChip; - try { - if (mNoChips) { - return null; - } - newChip = constructChipSpan(currentChip.getEntry(), true, false); - } catch (NullPointerException e) { - Log.e(TAG, e.getMessage(), e); - return null; - } - Editable editable = getText(); - QwertyKeyListener.markAsReplaced(editable, start, end, ""); - if (start == -1 || end == -1) { - Log.d(TAG, "The chip being selected no longer exists but should."); - } else { - editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - newChip.setSelected(true); - if (shouldShowEditableText(newChip)) { - scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip))); - } - showAddress(newChip, mAddressPopup, getWidth()); - setCursorVisible(false); - return newChip; - } else { - int start = getChipStart(currentChip); - int end = getChipEnd(currentChip); - getSpannable().removeSpan(currentChip); - DrawableRecipientChip newChip; - try { - newChip = constructChipSpan(currentChip.getEntry(), true, false); - } catch (NullPointerException e) { - Log.e(TAG, e.getMessage(), e); - return null; - } - Editable editable = getText(); - QwertyKeyListener.markAsReplaced(editable, start, end, ""); - if (start == -1 || end == -1) { - Log.d(TAG, "The chip being selected no longer exists but should."); - } else { - editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - newChip.setSelected(true); - if (shouldShowEditableText(newChip)) { - scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip))); - } - showAlternates(newChip, mAlternatesPopup, getWidth()); - setCursorVisible(false); - return newChip; - } - } - - private boolean shouldShowEditableText(DrawableRecipientChip currentChip) { - long contactId = currentChip.getContactId(); - return contactId == RecipientEntry.INVALID_CONTACT - || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT); - } - - private void showAddress(final DrawableRecipientChip currentChip, final ListPopupWindow popup, - int width) { - if (!mAttachedToWindow) { - return; - } - int line = getLayout().getLineForOffset(getChipStart(currentChip)); - int bottom = calculateOffsetFromBottom(line); - // Align the alternates popup with the left side of the View, - // regardless of the position of the chip tapped. - popup.setWidth(width); - popup.setAnchorView(this); - popup.setVerticalOffset(bottom); - popup.setAdapter(createSingleAddressAdapter(currentChip)); - popup.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - unselectChip(currentChip); - popup.dismiss(); - } - }); - popup.show(); - ListView listView = popup.getListView(); - listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); - listView.setItemChecked(0, true); - } - - /** - * Remove selection from this chip. Unselecting a RecipientChip will render - * the chip without a delete icon and with an unfocused background. This is - * called when the RecipientChip no longer has focus. - */ - private void unselectChip(DrawableRecipientChip chip) { - int start = getChipStart(chip); - int end = getChipEnd(chip); - Editable editable = getText(); - mSelectedChip = null; - if (start == -1 || end == -1) { - Log.w(TAG, "The chip doesn't exist or may be a chip a user was editing"); - setSelection(editable.length()); - commitDefault(); - } else { - getSpannable().removeSpan(chip); - QwertyKeyListener.markAsReplaced(editable, start, end, ""); - editable.removeSpan(chip); - try { - if (!mNoChips) { - editable.setSpan(constructChipSpan(chip.getEntry(), false, false), - start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } catch (NullPointerException e) { - Log.e(TAG, e.getMessage(), e); - } - } - setCursorVisible(true); - setSelection(editable.length()); - if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) { - mAlternatesPopup.dismiss(); - } - } - - /** - * Return whether a touch event was inside the delete target of - * a selected chip. It is in the delete target if: - * 1) the x and y points of the event are within the - * delete assset. - * 2) the point tapped would have caused a cursor to appear - * right after the selected chip. - * @return boolean - */ - 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? - if (mDisableDelete) { - return false; - } - - return chip.isSelected() && - ((mAvatarPosition == AVATAR_POSITION_END && offset == getChipEnd(chip)) || - (mAvatarPosition != AVATAR_POSITION_END && offset == getChipStart(chip))); - } - - /** - * Remove the chip and any text associated with it from the RecipientEditTextView. - */ - // Visible for testing. - /* package */void removeChip(DrawableRecipientChip chip) { - Spannable spannable = getSpannable(); - int spanStart = spannable.getSpanStart(chip); - int spanEnd = spannable.getSpanEnd(chip); - Editable text = getText(); - int toDelete = spanEnd; - boolean wasSelected = chip == mSelectedChip; - // Clear that there is a selected chip before updating any text. - if (wasSelected) { - mSelectedChip = null; - } - // Always remove trailing spaces when removing a chip. - while (toDelete >= 0 && toDelete < text.length() && text.charAt(toDelete) == ' ') { - toDelete++; - } - spannable.removeSpan(chip); - if (spanStart >= 0 && toDelete > 0) { - text.delete(spanStart, toDelete); - } - if (wasSelected) { - clearSelectedChip(); - } - } - - /** - * Replace this currently selected chip with a new chip - * that uses the contact data provided. - */ - // Visible for testing. - /*package*/ void replaceChip(DrawableRecipientChip chip, RecipientEntry entry) { - boolean wasSelected = chip == mSelectedChip; - if (wasSelected) { - mSelectedChip = null; - } - int start = getChipStart(chip); - int end = getChipEnd(chip); - getSpannable().removeSpan(chip); - Editable editable = getText(); - CharSequence chipText = createChip(entry, false); - if (chipText != null) { - if (start == -1 || end == -1) { - Log.e(TAG, "The chip to replace does not exist but should."); - editable.insert(0, chipText); - } else { - if (!TextUtils.isEmpty(chipText)) { - // There may be a space to replace with this chip's new - // associated space. Check for it - int toReplace = end; - while (toReplace >= 0 && toReplace < editable.length() - && editable.charAt(toReplace) == ' ') { - toReplace++; - } - editable.replace(start, toReplace, chipText); - } - } - } - setCursorVisible(true); - if (wasSelected) { - clearSelectedChip(); - } - } - - /** - * Handle click events for a chip. When a selected chip receives a click - * event, see if that event was in the delete icon. If so, delete it. - * Otherwise, unselect the chip. - */ - public void onClick(DrawableRecipientChip chip, int offset, float x, float y) { - if (chip.isSelected()) { - if (isInDelete(chip, offset, x, y)) { - removeChip(chip); - } else { - clearSelectedChip(); - } - } - } - - private boolean chipsPending() { - return mPendingChipsCount > 0 || (mRemovedSpans != null && mRemovedSpans.size() > 0); - } - - @Override - public void removeTextChangedListener(TextWatcher watcher) { - mTextWatcher = null; - super.removeTextChangedListener(watcher); - } - - private class RecipientTextWatcher implements TextWatcher { - - @Override - public void afterTextChanged(Editable s) { - // If the text has been set to null or empty, make sure we remove - // all the spans we applied. - if (TextUtils.isEmpty(s)) { - // Remove all the chips spans. - Spannable spannable = getSpannable(); - DrawableRecipientChip[] chips = spannable.getSpans(0, getText().length(), - DrawableRecipientChip.class); - for (DrawableRecipientChip chip : chips) { - spannable.removeSpan(chip); - } - if (mMoreChip != null) { - spannable.removeSpan(mMoreChip); - } - clearSelectedChip(); - return; - } - // Get whether there are any recipients pending addition to the - // view. If there are, don't do anything in the text watcher. - if (chipsPending()) { - return; - } - // If the user is editing a chip, don't clear it. - 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; - if (end != len) { - last = s.charAt(end); - } else { - last = s.charAt(len); - } - if (last == COMMIT_CHAR_SPACE) { - if (!isPhoneQuery()) { - // Check if this is a valid email address. If it is, - // commit it. - String text = getText().toString(); - int tokenStart = mTokenizer.findTokenStart(text, getSelectionEnd()); - String sub = text.substring(tokenStart, mTokenizer.findTokenEnd(text, - tokenStart)); - if (!TextUtils.isEmpty(sub) && mValidator != null && - mValidator.isValid(sub)) { - commitByCharacter(); - } - } - } - } - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // 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(); - DrawableRecipientChip[] repl = getSpannable().getSpans(selStart, selStart, - DrawableRecipientChip.class); - if (repl.length > 0) { - // There is a chip there! Just remove it. - Editable editable = getText(); - // Add the separator token. - int tokenStart = mTokenizer.findTokenStart(editable, selStart); - int tokenEnd = mTokenizer.findTokenEnd(editable, tokenStart); - tokenEnd = tokenEnd + 1; - if (tokenEnd > editable.length()) { - tokenEnd = editable.length(); - } - editable.delete(tokenStart, tokenEnd); - getSpannable().removeSpan(repl[0]); - } - } else if (count > before) { - if (mSelectedChip != null - && isGeneratedContact(mSelectedChip)) { - if (lastCharacterIsCommitCharacter(s)) { - commitByCharacter(); - return; - } - } - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing. - } - } - - 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}. - */ - private void handlePasteClip(ClipData clip) { - removeTextChangedListener(mTextWatcher); - - if (clip != null && clip.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)){ - for (int i = 0; i < clip.getItemCount(); i++) { - CharSequence paste = clip.getItemAt(i).getText(); - if (paste != null) { - int start = getSelectionStart(); - int end = getSelectionEnd(); - Editable editable = getText(); - if (start >= 0 && end >= 0 && start != end) { - editable.append(paste, start, end); - } else { - editable.insert(end, paste); - } - handlePasteAndReplace(); - } - } - } - - mHandler.post(mAddTextWatcher); - } - - @Override - public boolean onTextContextMenuItem(int id) { - if (id == android.R.id.paste) { - ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService( - Context.CLIPBOARD_SERVICE); - handlePasteClip(clipboard.getPrimaryClip()); - return true; - } - return super.onTextContextMenuItem(id); - } - - private void handlePasteAndReplace() { - ArrayList<DrawableRecipientChip> created = handlePaste(); - if (created != null && created.size() > 0) { - // Perform reverse lookups on the pasted contacts. - IndividualReplacementTask replace = new IndividualReplacementTask(); - replace.execute(created); - } - } - - // Visible for testing. - /* package */ArrayList<DrawableRecipientChip> handlePaste() { - String text = getText().toString(); - int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd()); - String lastAddress = text.substring(originalTokenStart); - int tokenStart = originalTokenStart; - 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 && 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; - DrawableRecipientChip createdChip; - while (tokenStart < originalTokenStart) { - tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(getText().toString(), - tokenStart)); - commitChip(tokenStart, tokenEnd, getText()); - createdChip = findChip(tokenStart); - if (createdChip == null) { - break; - } - // +1 for the space at the end. - tokenStart = getSpannable().getSpanEnd(createdChip) + 1; - created.add(createdChip); - } - } - } - // Take a look at the last token. If the token has been completed with a - // commit character, create a chip. - if (isCompletedToken(lastAddress)) { - Editable editable = getText(); - tokenStart = editable.toString().indexOf(lastAddress, originalTokenStart); - commitChip(tokenStart, editable.length(), editable); - created.add(findChip(tokenStart)); - } - return created; - } - - // Visible for testing. - /* package */int movePastTerminators(int tokenEnd) { - if (tokenEnd >= length()) { - return tokenEnd; - } - char atEnd = getText().toString().charAt(tokenEnd); - if (atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON) { - tokenEnd++; - } - // This token had not only an end token character, but also a space - // separating it from the next token. - if (tokenEnd < length() && getText().toString().charAt(tokenEnd) == ' ') { - tokenEnd++; - } - return tokenEnd; - } - - private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> { - private DrawableRecipientChip createFreeChip(RecipientEntry entry) { - try { - if (mNoChips) { - return null; - } - return constructChipSpan(entry, false, - false /*leave space for contact icon */); - } catch (NullPointerException e) { - Log.e(TAG, e.getMessage(), e); - return null; - } - } - - @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); - } - // 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<DrawableRecipientChip> recipients = - new ArrayList<DrawableRecipientChip>(); - DrawableRecipientChip[] existingChips = getSortedRecipients(); - for (int i = 0; i < existingChips.length; i++) { - recipients.add(existingChips[i]); - } - if (mRemovedSpans != null) { - recipients.addAll(mRemovedSpans); - } - ArrayList<String> addresses = new ArrayList<String>(); - DrawableRecipientChip chip; - for (int i = 0; i < recipients.size(); i++) { - chip = recipients.get(i); - if (chip != null) { - addresses.add(createAddressText(chip.getEntry())); - } - } - final BaseRecipientAdapter adapter = getAdapter(); - RecipientAlternatesAdapter.getMatchingRecipients(getContext(), adapter, 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) { - final Runnable runnable = new Runnable() { - @Override - public void run() { - final Editable text = new SpannableStringBuilder(getText()); - int i = 0; - 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++; - } - setText(text); - } - }; - - if (Looper.myLooper() == Looper.getMainLooper()) { - runnable.run(); - } else { - mHandler.post(runnable); - } - } - } - } - - private class IndividualReplacementTask - extends AsyncTask<ArrayList<DrawableRecipientChip>, Void, Void> { - @Override - 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<DrawableRecipientChip> originalRecipients = params[0]; - ArrayList<String> addresses = new ArrayList<String>(); - DrawableRecipientChip chip; - for (int i = 0; i < originalRecipients.size(); i++) { - chip = originalRecipients.get(i); - if (chip != null) { - addresses.add(createAddressText(chip.getEntry())); - } - } - final BaseRecipientAdapter adapter = getAdapter(); - RecipientAlternatesAdapter.getMatchingRecipients(getContext(), adapter, addresses, - adapter.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. - final RecipientEntry entry = createValidatedEntry(entries - .get(tokenizeAddress(temp.getEntry().getDestination()) - .toLowerCase())); - if (entry != null) { - mHandler.post(new Runnable() { - @Override - public void run() { - replaceChip(temp, entry); - } - }); - } - } - } - } - - @Override - public void matchesNotFound(final Set<String> unfoundAddresses) { - // No action required - } - }); - return null; - } - } - - - /** - * MoreImageSpan is a simple class created for tracking the existence of a - * more chip across activity restarts/ - */ - private class MoreImageSpan extends ImageSpan { - public MoreImageSpan(Drawable b) { - super(b); - } - } - - @Override - public boolean onDown(MotionEvent e) { - return false; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - // Do nothing. - return false; - } - - @Override - public void onLongPress(MotionEvent event) { - if (mSelectedChip != null) { - return; - } - float x = event.getX(); - float y = event.getY(); - final int offset = putOffsetInRange(x, y); - DrawableRecipientChip currentChip = findChip(offset); - if (currentChip != null) { - if (mDragEnabled) { - // Start drag-and-drop for the selected chip. - startDrag(currentChip); - } else { - // Copy the selected chip email address. - showCopyDialog(currentChip.getEntry().getDestination()); - } - } - } - - // The following methods are used to provide some functionality on older versions of Android - // These methods were copied out of JB MR2's TextView - ///////////////////////////////////////////////// - private int supportGetOffsetForPosition(float x, float y) { - if (getLayout() == null) return -1; - final int line = supportGetLineAtCoordinate(y); - final int offset = supportGetOffsetAtCoordinate(line, x); - return offset; - } - - private float supportConvertToLocalHorizontalCoordinate(float x) { - x -= getTotalPaddingLeft(); - // Clamp the position to inside of the view. - x = Math.max(0.0f, x); - x = Math.min(getWidth() - getTotalPaddingRight() - 1, x); - x += getScrollX(); - return x; - } - - private int supportGetLineAtCoordinate(float y) { - y -= getTotalPaddingLeft(); - // Clamp the position to inside of the view. - y = Math.max(0.0f, y); - y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); - y += getScrollY(); - return getLayout().getLineForVertical((int) y); - } - - private int supportGetOffsetAtCoordinate(int line, float x) { - x = supportConvertToLocalHorizontalCoordinate(x); - return getLayout().getOffsetForHorizontal(line, x); - } - ///////////////////////////////////////////////// - - /** - * Enables drag-and-drop for chips. - */ - public void enableDrag() { - mDragEnabled = true; - } - - /** - * Starts drag-and-drop for the selected chip. - */ - private void startDrag(DrawableRecipientChip currentChip) { - String address = currentChip.getEntry().getDestination(); - ClipData data = ClipData.newPlainText(address, address + COMMIT_CHAR_COMMA); - - // Start drag mode. - startDrag(data, new RecipientChipShadow(currentChip), null, 0); - - // Remove the current chip, so drag-and-drop will result in a move. - // TODO (phamm): consider readd this chip if it's dropped outside a target. - removeChip(currentChip); - } - - /** - * Handles drag event. - */ - @Override - public boolean onDragEvent(DragEvent event) { - switch (event.getAction()) { - case DragEvent.ACTION_DRAG_STARTED: - // Only handle plain text drag and drop. - return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN); - case DragEvent.ACTION_DRAG_ENTERED: - requestFocus(); - return true; - case DragEvent.ACTION_DROP: - handlePasteClip(event.getClipData()); - return true; - } - return false; - } - - /** - * Drag shadow for a {@link RecipientChip}. - */ - private final class RecipientChipShadow extends DragShadowBuilder { - private final DrawableRecipientChip mChip; - - public RecipientChipShadow(DrawableRecipientChip chip) { - mChip = chip; - } - - @Override - public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { - Rect rect = mChip.getBounds(); - shadowSize.set(rect.width(), rect.height()); - shadowTouchPoint.set(rect.centerX(), rect.centerY()); - } - - @Override - public void onDrawShadow(Canvas canvas) { - mChip.draw(canvas); - } - } - - private void showCopyDialog(final String address) { - if (!mAttachedToWindow) { - return; - } - mCopyAddress = address; - mCopyDialog.setTitle(address); - mCopyDialog.setContentView(R.layout.copy_chip_dialog_layout); - mCopyDialog.setCancelable(true); - mCopyDialog.setCanceledOnTouchOutside(true); - Button button = (Button)mCopyDialog.findViewById(android.R.id.button1); - button.setOnClickListener(this); - int btnTitleId; - if (isPhoneQuery()) { - btnTitleId = R.string.copy_number; - } else { - btnTitleId = R.string.copy_email; - } - String buttonTitle = getContext().getResources().getString(btnTitleId); - button.setText(buttonTitle); - mCopyDialog.setOnDismissListener(this); - mCopyDialog.show(); - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - // Do nothing. - return false; - } - - @Override - public void onShowPress(MotionEvent e) { - // Do nothing. - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - // Do nothing. - return false; - } - - @Override - public void onDismiss(DialogInterface dialog) { - mCopyAddress = null; - } - - @Override - public void onClick(View v) { - // Copy this to the clipboard. - ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService( - Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(ClipData.newPlainText("", mCopyAddress)); - mCopyDialog.dismiss(); - } - - protected boolean isPhoneQuery() { - return getAdapter() != null - && getAdapter().getQueryType() == BaseRecipientAdapter.QUERY_TYPE_PHONE; - } - - @Override - public BaseRecipientAdapter getAdapter() { - return (BaseRecipientAdapter) super.getAdapter(); - } -} diff --git a/chips/src/com/android/ex/chips/RecipientEntry.java b/chips/src/com/android/ex/chips/RecipientEntry.java deleted file mode 100644 index 7d9b87f..0000000 --- a/chips/src/com/android/ex/chips/RecipientEntry.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * 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; - -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. - */ -public class RecipientEntry { - /* package */ static final int INVALID_CONTACT = -1; - /** - * A GENERATED_CONTACT is one that was created based entirely on - * information passed in to the RecipientEntry from an external source - * that is not a real contact. - */ - /* package */ static final int GENERATED_CONTACT = -2; - - /** Used when {@link #mDestinationType} is invalid and thus shouldn't be used for display. */ - /* package */ static final int INVALID_DESTINATION_TYPE = -1; - - public static final int ENTRY_TYPE_PERSON = 0; - - public static final int ENTRY_TYPE_SIZE = 1; - - private final int mEntryType; - - /** - * True when this entry is the first entry in a group, which should have a photo and display - * name, while the second or later entries won't. - */ - private boolean mIsFirstLevel; - private final String mDisplayName; - - /** Destination for this contact entry. Would be an email address or a phone number. */ - private final String mDestination; - /** Type of the destination like {@link Email#TYPE_HOME} */ - private final int mDestinationType; - /** - * Label of the destination which will be used when type was {@link Email#TYPE_CUSTOM}. - * Can be null when {@link #mDestinationType} is {@link #INVALID_DESTINATION_TYPE}. - */ - private final String mDestinationLabel; - /** ID for the person */ - private final long mContactId; - /** ID for the directory this contact came from, or <code>null</code> */ - private final Long mDirectoryId; - /** ID for the destination */ - private final long mDataId; - private final boolean mIsDivider; - - 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; - - /** See {@link ContactsContract.Contacts#LOOKUP_KEY} */ - private final String mLookupKey; - - private RecipientEntry(int entryType, String displayName, String destination, - int destinationType, String destinationLabel, long contactId, Long directoryId, - long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid, - String lookupKey) { - mEntryType = entryType; - mIsFirstLevel = isFirstLevel; - mDisplayName = displayName; - mDestination = destination; - mDestinationType = destinationType; - mDestinationLabel = destinationLabel; - mContactId = contactId; - mDirectoryId = directoryId; - mDataId = dataId; - mPhotoThumbnailUri = photoThumbnailUri; - mPhotoBytes = null; - mIsDivider = false; - mIsValid = isValid; - mLookupKey = lookupKey; - } - - public boolean isValid() { - return mIsValid; - } - - /** - * Determine if this was a RecipientEntry created from recipient info or - * an entry from contacts. - */ - public static boolean isCreatedRecipient(long id) { - return id == RecipientEntry.INVALID_CONTACT || id == RecipientEntry.GENERATED_CONTACT; - } - - /** - * Construct a RecipientEntry from just an address that has been entered. - * This address has not been resolved to a contact and therefore does not - * have a contact id or photo. - */ - 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, null /* directoryId */, - INVALID_CONTACT, null, true, isValid, null /* lookupKey */); - } - - /** - * 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, null /* directoryId */, - INVALID_CONTACT, null, true, isValid, null /* lookupKey */); - } - - /** - * @return the display name for the entry. If the display name source is larger than - * {@link DisplayNameSources#PHONE} we use the contact's display name, but if not, - * i.e. the display name came from an email address or a phone number, we don't use it - * to avoid confusion and just use the destination instead. - */ - private static String pickDisplayName(int displayNameSource, String displayName, - String destination) { - return (displayNameSource > DisplayNameSources.PHONE) ? displayName : destination; - } - - /** - * Construct a RecipientEntry from just an address that has been entered - * 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, - boolean isValid) { - return new RecipientEntry(ENTRY_TYPE_PERSON, display, address, INVALID_DESTINATION_TYPE, - null, GENERATED_CONTACT, null /* directoryId */, GENERATED_CONTACT, null, true, - isValid, null /* lookupKey */); - } - - public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource, - String destination, int destinationType, String destinationLabel, long contactId, - Long directoryId, long dataId, Uri photoThumbnailUri, boolean isValid, - String lookupKey) { - return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource, - displayName, destination), destination, destinationType, destinationLabel, - contactId, directoryId, dataId, photoThumbnailUri, true, isValid, lookupKey); - } - - public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource, - String destination, int destinationType, String destinationLabel, long contactId, - Long directoryId, long dataId, String thumbnailUriAsString, boolean isValid, - String lookupKey) { - return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource, - displayName, destination), destination, destinationType, destinationLabel, - contactId, directoryId, dataId, (thumbnailUriAsString != null - ? Uri.parse(thumbnailUriAsString) : null), true, isValid, lookupKey); - } - - public static RecipientEntry constructSecondLevelEntry(String displayName, - int displayNameSource, String destination, int destinationType, - String destinationLabel, long contactId, Long directoryId, long dataId, - String thumbnailUriAsString, boolean isValid, String lookupKey) { - return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource, - displayName, destination), destination, destinationType, destinationLabel, - contactId, directoryId, dataId, (thumbnailUriAsString != null - ? Uri.parse(thumbnailUriAsString) : null), false, isValid, lookupKey); - } - - public int getEntryType() { - return mEntryType; - } - - public String getDisplayName() { - return mDisplayName; - } - - public String getDestination() { - return mDestination; - } - - public int getDestinationType() { - return mDestinationType; - } - - public String getDestinationLabel() { - return mDestinationLabel; - } - - public long getContactId() { - return mContactId; - } - - public Long getDirectoryId() { - return mDirectoryId; - } - - public long getDataId() { - return mDataId; - } - - public boolean isFirstLevel() { - return mIsFirstLevel; - } - - public Uri getPhotoThumbnailUri() { - return mPhotoThumbnailUri; - } - - /** This can be called outside main Looper thread. */ - public synchronized void setPhotoBytes(byte[] photoBytes) { - mPhotoBytes = photoBytes; - } - - /** This can be called outside main Looper thread. */ - public synchronized byte[] getPhotoBytes() { - return mPhotoBytes; - } - - public boolean isSeparator() { - return mIsDivider; - } - - public boolean isSelectable() { - return mEntryType == ENTRY_TYPE_PERSON; - } - - public String getLookupKey() { - return mLookupKey; - } - - @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 deleted file mode 100644 index 985953f..0000000 --- a/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; - -import com.android.ex.chips.DropdownChipLayouter.AdapterType; - -class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry> { - private final DropdownChipLayouter mDropdownChipLayouter; - - public SingleRecipientArrayAdapter(Context context, RecipientEntry entry, - DropdownChipLayouter dropdownChipLayouter) { - super(context, dropdownChipLayouter.getAlternateItemLayoutResId(), new RecipientEntry[] { - entry - }); - - mDropdownChipLayouter = dropdownChipLayouter; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return mDropdownChipLayouter.bindView(convertView, parent, getItem(position), position, - AdapterType.SINGLE_RECIPIENT, null); - } -} diff --git a/chips/src/com/android/ex/chips/recipientchip/BaseRecipientChip.java b/chips/src/com/android/ex/chips/recipientchip/BaseRecipientChip.java deleted file mode 100644 index 8012b5c..0000000 --- a/chips/src/com/android/ex/chips/recipientchip/BaseRecipientChip.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 directory id of the contact associated with this chip. - */ - Long getDirectoryId(); - - /** - * Get the directory lookup key associated with this chip, or <code>null</code>. - */ - String getLookupKey(); - - /** - * 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 deleted file mode 100644 index 396a8ac..0000000 --- a/chips/src/com/android/ex/chips/recipientchip/DrawableRecipientChip.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 deleted file mode 100644 index 455f2cb..0000000 --- a/chips/src/com/android/ex/chips/recipientchip/InvisibleRecipientChip.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 getDirectoryId() { - return mDelegate.getDirectoryId(); - } - - @Override - public String getLookupKey() { - return mDelegate.getLookupKey(); - } - - @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/SimpleRecipientChip.java b/chips/src/com/android/ex/chips/recipientchip/SimpleRecipientChip.java deleted file mode 100644 index 533f53f..0000000 --- a/chips/src/com/android/ex/chips/recipientchip/SimpleRecipientChip.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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; - -import android.text.TextUtils; - -class SimpleRecipientChip implements BaseRecipientChip { - private final CharSequence mDisplay; - - private final CharSequence mValue; - - private final long mContactId; - - private final Long mDirectoryId; - - private final String mLookupKey; - - private final long mDataId; - - private final RecipientEntry mEntry; - - private boolean mSelected = false; - - private CharSequence mOriginalText; - - public SimpleRecipientChip(final RecipientEntry entry) { - mDisplay = entry.getDisplayName(); - mValue = entry.getDestination().trim(); - mContactId = entry.getContactId(); - mDirectoryId = entry.getDirectoryId(); - mLookupKey = entry.getLookupKey(); - mDataId = entry.getDataId(); - mEntry = entry; - } - - @Override - public void setSelected(final boolean selected) { - mSelected = selected; - } - - @Override - public boolean isSelected() { - return mSelected; - } - - @Override - public CharSequence getDisplay() { - return mDisplay; - } - - @Override - public CharSequence getValue() { - return mValue; - } - - @Override - public long getContactId() { - return mContactId; - } - - @Override - public Long getDirectoryId() { - return mDirectoryId; - } - - @Override - public String getLookupKey() { - return mLookupKey; - } - - @Override - public long getDataId() { - return mDataId; - } - - @Override - public RecipientEntry getEntry() { - return mEntry; - } - - @Override - public void setOriginalText(final String text) { - if (TextUtils.isEmpty(text)) { - mOriginalText = text; - } else { - mOriginalText = text.trim(); - } - } - - @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 deleted file mode 100644 index 6d3d27d..0000000 --- a/chips/src/com/android/ex/chips/recipientchip/VisibleRecipientChip.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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) { - this(drawable, entry, DynamicDrawableSpan.ALIGN_BOTTOM); - } - - public VisibleRecipientChip(final Drawable drawable, final RecipientEntry entry, - final int verticalAlignment) { - super(drawable, verticalAlignment); - - 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 getDirectoryId() { - return mDelegate.getDirectoryId(); - } - - @Override - public String getLookupKey() { - return mDelegate.getLookupKey(); - } - - @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(); - } -} |