Home | History | Annotate | Download | only in chips
      1 /*
      2 
      3  * Copyright (C) 2011 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.ex.chips;
     19 
     20 import android.annotation.TargetApi;
     21 import android.app.Activity;
     22 import android.app.DialogFragment;
     23 import android.content.ClipData;
     24 import android.content.ClipDescription;
     25 import android.content.ClipboardManager;
     26 import android.content.Context;
     27 import android.content.res.Resources;
     28 import android.content.res.TypedArray;
     29 import android.graphics.Bitmap;
     30 import android.graphics.BitmapFactory;
     31 import android.graphics.BitmapShader;
     32 import android.graphics.Canvas;
     33 import android.graphics.Color;
     34 import android.graphics.Matrix;
     35 import android.graphics.Paint;
     36 import android.graphics.Paint.Style;
     37 import android.graphics.Point;
     38 import android.graphics.Rect;
     39 import android.graphics.RectF;
     40 import android.graphics.Shader.TileMode;
     41 import android.graphics.drawable.BitmapDrawable;
     42 import android.graphics.drawable.Drawable;
     43 import android.graphics.drawable.StateListDrawable;
     44 import android.os.AsyncTask;
     45 import android.os.Build;
     46 import android.os.Handler;
     47 import android.os.Looper;
     48 import android.os.Message;
     49 import android.os.Parcelable;
     50 import android.support.annotation.NonNull;
     51 import android.text.Editable;
     52 import android.text.InputType;
     53 import android.text.Layout;
     54 import android.text.Spannable;
     55 import android.text.SpannableString;
     56 import android.text.SpannableStringBuilder;
     57 import android.text.Spanned;
     58 import android.text.TextPaint;
     59 import android.text.TextUtils;
     60 import android.text.TextWatcher;
     61 import android.text.method.QwertyKeyListener;
     62 import android.text.util.Rfc822Token;
     63 import android.text.util.Rfc822Tokenizer;
     64 import android.util.AttributeSet;
     65 import android.util.Log;
     66 import android.view.ActionMode;
     67 import android.view.ActionMode.Callback;
     68 import android.view.DragEvent;
     69 import android.view.GestureDetector;
     70 import android.view.KeyEvent;
     71 import android.view.LayoutInflater;
     72 import android.view.Menu;
     73 import android.view.MenuItem;
     74 import android.view.MotionEvent;
     75 import android.view.View;
     76 import android.view.ViewParent;
     77 import android.view.accessibility.AccessibilityEvent;
     78 import android.view.accessibility.AccessibilityManager;
     79 import android.view.inputmethod.EditorInfo;
     80 import android.view.inputmethod.InputConnection;
     81 import android.widget.AdapterView;
     82 import android.widget.AdapterView.OnItemClickListener;
     83 import android.widget.Filterable;
     84 import android.widget.ListAdapter;
     85 import android.widget.ListPopupWindow;
     86 import android.widget.ListView;
     87 import android.widget.MultiAutoCompleteTextView;
     88 import android.widget.PopupWindow;
     89 import android.widget.ScrollView;
     90 import android.widget.TextView;
     91 
     92 import com.android.ex.chips.DropdownChipLayouter.PermissionRequestDismissedListener;
     93 import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
     94 import com.android.ex.chips.recipientchip.DrawableRecipientChip;
     95 import com.android.ex.chips.recipientchip.InvisibleRecipientChip;
     96 import com.android.ex.chips.recipientchip.ReplacementDrawableSpan;
     97 import com.android.ex.chips.recipientchip.VisibleRecipientChip;
     98 
     99 import java.util.ArrayList;
    100 import java.util.Arrays;
    101 import java.util.Collections;
    102 import java.util.Comparator;
    103 import java.util.List;
    104 import java.util.Map;
    105 import java.util.Set;
    106 
    107 /**
    108  * RecipientEditTextView is an auto complete text view for use with applications
    109  * that use the new Chips UI for addressing a message to recipients.
    110  */
    111 public class RecipientEditTextView extends MultiAutoCompleteTextView implements
    112         OnItemClickListener, Callback, RecipientAlternatesAdapter.OnCheckedItemChangedListener,
    113         GestureDetector.OnGestureListener, TextView.OnEditorActionListener,
    114         DropdownChipLayouter.ChipDeleteListener, PermissionRequestDismissedListener {
    115     private static final String TAG = "RecipientEditTextView";
    116 
    117     private static final char COMMIT_CHAR_COMMA = ',';
    118     private static final char COMMIT_CHAR_SEMICOLON = ';';
    119     private static final char COMMIT_CHAR_SPACE = ' ';
    120     private static final String SEPARATOR = String.valueOf(COMMIT_CHAR_COMMA)
    121             + String.valueOf(COMMIT_CHAR_SPACE);
    122 
    123     private static final int DISMISS = "dismiss".hashCode();
    124     private static final long DISMISS_DELAY = 300;
    125 
    126     // TODO: get correct number/ algorithm from with UX.
    127     // Visible for testing.
    128     /*package*/ static final int CHIP_LIMIT = 2;
    129 
    130     private static final int MAX_CHIPS_PARSED = 50;
    131 
    132     private int mUnselectedChipTextColor;
    133     private int mUnselectedChipBackgroundColor;
    134 
    135     // Work variables to avoid re-allocation on every typed character.
    136     private final Rect mRect = new Rect();
    137     private final int[] mCoords = new int[2];
    138 
    139     // Resources for displaying chips.
    140     private Drawable mChipBackground = null;
    141     private Drawable mChipDelete = null;
    142     private Drawable mInvalidChipBackground;
    143 
    144     // Possible attr overrides
    145     private float mChipHeight;
    146     private float mChipFontSize;
    147     private float mLineSpacingExtra;
    148     private int mChipTextStartPadding;
    149     private int mChipTextEndPadding;
    150     private final int mTextHeight;
    151     private boolean mDisableDelete;
    152     private int mMaxLines;
    153 
    154     /**
    155      * Enumerator for avatar position. See attr.xml for more details.
    156      * 0 for end, 1 for start.
    157      */
    158     private int mAvatarPosition;
    159     private static final int AVATAR_POSITION_END = 0;
    160     private static final int AVATAR_POSITION_START = 1;
    161 
    162     private Paint mWorkPaint = new Paint();
    163 
    164     private Tokenizer mTokenizer;
    165     private Validator mValidator;
    166     private Handler mHandler;
    167     private TextWatcher mTextWatcher;
    168     protected DropdownChipLayouter mDropdownChipLayouter;
    169 
    170     private View mDropdownAnchor = this;
    171     private ListPopupWindow mAlternatesPopup;
    172     private ListPopupWindow mAddressPopup;
    173     private View mAlternatePopupAnchor;
    174     private OnItemClickListener mAlternatesListener;
    175 
    176     private DrawableRecipientChip mSelectedChip;
    177     private Bitmap mDefaultContactPhoto;
    178     private ReplacementDrawableSpan mMoreChip;
    179     private TextView mMoreItem;
    180 
    181     private int mCurrentSuggestionCount;
    182 
    183     // VisibleForTesting
    184     final ArrayList<String> mPendingChips = new ArrayList<String>();
    185 
    186     private int mPendingChipsCount = 0;
    187     private int mCheckedItem;
    188     private boolean mNoChipMode = false;
    189     private boolean mShouldShrink = true;
    190     private boolean mRequiresShrinkWhenNotGone = false;
    191 
    192     // VisibleForTesting
    193     ArrayList<DrawableRecipientChip> mTemporaryRecipients;
    194 
    195     private ArrayList<DrawableRecipientChip> mHiddenSpans;
    196 
    197     // Chip copy fields.
    198     private GestureDetector mGestureDetector;
    199 
    200     // Obtain the enclosing scroll view, if it exists, so that the view can be
    201     // scrolled to show the last line of chips content.
    202     private ScrollView mScrollView;
    203     private boolean mTriedGettingScrollView;
    204     private boolean mDragEnabled = false;
    205 
    206     private boolean mAttachedToWindow;
    207 
    208     private final Runnable mAddTextWatcher = new Runnable() {
    209         @Override
    210         public void run() {
    211             if (mTextWatcher == null) {
    212                 mTextWatcher = new RecipientTextWatcher();
    213                 addTextChangedListener(mTextWatcher);
    214             }
    215         }
    216     };
    217 
    218     private IndividualReplacementTask mIndividualReplacements;
    219 
    220     private Runnable mHandlePendingChips = new Runnable() {
    221 
    222         @Override
    223         public void run() {
    224             handlePendingChips();
    225         }
    226 
    227     };
    228 
    229     private Runnable mDelayedShrink = new Runnable() {
    230 
    231         @Override
    232         public void run() {
    233             shrink();
    234         }
    235 
    236     };
    237 
    238     private RecipientEntryItemClickedListener mRecipientEntryItemClickedListener;
    239 
    240     private RecipientChipAddedListener mRecipientChipAddedListener;
    241     private RecipientChipDeletedListener mRecipientChipDeletedListener;
    242 
    243     public interface RecipientEntryItemClickedListener {
    244         /**
    245          * Callback that occurs whenever an auto-complete suggestion is clicked.
    246          * @param charactersTyped the number of characters typed by the user to provide the
    247          *                        auto-complete suggestions.
    248          * @param position the position in the dropdown list that the user clicked
    249          */
    250         void onRecipientEntryItemClicked(int charactersTyped, int position);
    251     }
    252 
    253     private PermissionsRequestItemClickedListener mPermissionsRequestItemClickedListener;
    254 
    255     /**
    256      * Listener for handling clicks on the {@link RecipientEntry} that have
    257      * {@link RecipientEntry#ENTRY_TYPE_PERMISSION_REQUEST} type.
    258      */
    259     public interface PermissionsRequestItemClickedListener {
    260 
    261         /**
    262          * Callback that occurs when user clicks the item that asks user to grant permissions to
    263          * the app.
    264          *
    265          * @param view View that asks for permission.
    266          */
    267         void onPermissionsRequestItemClicked(RecipientEditTextView view, String[] permissions);
    268 
    269         /**
    270          * Callback that occurs when user dismisses the item that asks user to grant permissions to
    271          * the app.
    272          */
    273         void onPermissionRequestDismissed();
    274     }
    275 
    276     /**
    277      * Listener for handling deletion of chips in the recipient edit text.
    278      */
    279     public interface RecipientChipDeletedListener {
    280         /**
    281          * Callback that occurs when a chip is deleted.
    282          * @param entry RecipientEntry that contains information about the chip.
    283          */
    284         void onRecipientChipDeleted(RecipientEntry entry);
    285     }
    286 
    287     /**
    288      * Listener for handling addition of chips in the recipient edit text.
    289      */
    290     public interface RecipientChipAddedListener {
    291         /**
    292          * Callback that occurs when a chip is added.
    293          *
    294          * @param entry RecipientEntry that contains information about the chip.
    295          */
    296         void onRecipientChipAdded(RecipientEntry entry);
    297     }
    298 
    299     public RecipientEditTextView(Context context, AttributeSet attrs) {
    300         super(context, attrs);
    301         setChipDimensions(context, attrs);
    302         mTextHeight = calculateTextHeight();
    303         mAlternatesPopup = new ListPopupWindow(context);
    304         setupPopupWindow(mAlternatesPopup);
    305         mAddressPopup = new ListPopupWindow(context);
    306         setupPopupWindow(mAddressPopup);
    307         mAlternatesListener = new OnItemClickListener() {
    308             @Override
    309             public void onItemClick(AdapterView<?> adapterView,View view, int position,
    310                     long rowId) {
    311                 mAlternatesPopup.setOnItemClickListener(null);
    312                 replaceChip(mSelectedChip, ((RecipientAlternatesAdapter) adapterView.getAdapter())
    313                         .getRecipientEntry(position));
    314                 Message delayed = Message.obtain(mHandler, DISMISS);
    315                 delayed.obj = mAlternatesPopup;
    316                 mHandler.sendMessageDelayed(delayed, DISMISS_DELAY);
    317                 clearComposingText();
    318             }
    319         };
    320         setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    321         setOnItemClickListener(this);
    322         setCustomSelectionActionModeCallback(this);
    323         mHandler = new Handler() {
    324             @Override
    325             public void handleMessage(Message msg) {
    326                 if (msg.what == DISMISS) {
    327                     ((ListPopupWindow) msg.obj).dismiss();
    328                     return;
    329                 }
    330                 super.handleMessage(msg);
    331             }
    332         };
    333         mTextWatcher = new RecipientTextWatcher();
    334         addTextChangedListener(mTextWatcher);
    335         mGestureDetector = new GestureDetector(context, this);
    336         setOnEditorActionListener(this);
    337 
    338         setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context));
    339     }
    340 
    341     private void setupPopupWindow(ListPopupWindow popup) {
    342         popup.setOnDismissListener(new PopupWindow.OnDismissListener() {
    343             @Override
    344             public void onDismiss() {
    345                 clearSelectedChip();
    346             }
    347         });
    348     }
    349 
    350     private int calculateTextHeight() {
    351         final TextPaint paint = getPaint();
    352 
    353         mRect.setEmpty();
    354         // First measure the bounds of a sample text.
    355         final String textHeightSample = "a";
    356         paint.getTextBounds(textHeightSample, 0, textHeightSample.length(), mRect);
    357 
    358         mRect.left = 0;
    359         mRect.right = 0;
    360 
    361         return mRect.height();
    362     }
    363 
    364     public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
    365         mDropdownChipLayouter = dropdownChipLayouter;
    366         mDropdownChipLayouter.setDeleteListener(this);
    367         mDropdownChipLayouter.setPermissionRequestDismissedListener(this);
    368     }
    369 
    370     public void setRecipientEntryItemClickedListener(RecipientEntryItemClickedListener listener) {
    371         mRecipientEntryItemClickedListener = listener;
    372     }
    373 
    374     public void setPermissionsRequestItemClickedListener(
    375             PermissionsRequestItemClickedListener listener) {
    376         mPermissionsRequestItemClickedListener = listener;
    377     }
    378 
    379     public void setRecipientChipAddedListener(RecipientChipAddedListener listener) {
    380         mRecipientChipAddedListener = listener;
    381     }
    382 
    383     public void setRecipientChipDeletedListener(RecipientChipDeletedListener listener) {
    384         mRecipientChipDeletedListener = listener;
    385     }
    386 
    387     @Override
    388     protected void onDetachedFromWindow() {
    389         super.onDetachedFromWindow();
    390         mAttachedToWindow = false;
    391     }
    392 
    393     @Override
    394     protected void onAttachedToWindow() {
    395         super.onAttachedToWindow();
    396         mAttachedToWindow = true;
    397 
    398         final int anchorId = getDropDownAnchor();
    399         if (anchorId != View.NO_ID) {
    400             mDropdownAnchor = getRootView().findViewById(anchorId);
    401         }
    402     }
    403 
    404     @Override
    405     public void setDropDownAnchor(int anchorId) {
    406         super.setDropDownAnchor(anchorId);
    407         if (anchorId != View.NO_ID) {
    408           mDropdownAnchor = getRootView().findViewById(anchorId);
    409         }
    410     }
    411 
    412     @Override
    413     public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
    414         if (action == EditorInfo.IME_ACTION_DONE) {
    415             if (commitDefault()) {
    416                 return true;
    417             }
    418             if (mSelectedChip != null) {
    419                 clearSelectedChip();
    420                 return true;
    421             } else if (hasFocus()) {
    422                 if (focusNext()) {
    423                     return true;
    424                 }
    425             }
    426         }
    427         return false;
    428     }
    429 
    430     @Override
    431     public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
    432         InputConnection connection = super.onCreateInputConnection(outAttrs);
    433         int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION;
    434         if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) {
    435             // clear the existing action
    436             outAttrs.imeOptions ^= imeActions;
    437             // set the DONE action
    438             outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
    439         }
    440         if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
    441             outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
    442         }
    443 
    444         outAttrs.actionId = EditorInfo.IME_ACTION_DONE;
    445 
    446         // Custom action labels are discouraged in L; a checkmark icon is shown in place of the
    447         // custom text in this case.
    448         outAttrs.actionLabel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? null :
    449             getContext().getString(R.string.action_label);
    450         return connection;
    451     }
    452 
    453     /*package*/ DrawableRecipientChip getLastChip() {
    454         DrawableRecipientChip last = null;
    455         DrawableRecipientChip[] chips = getSortedRecipients();
    456         if (chips != null && chips.length > 0) {
    457             last = chips[chips.length - 1];
    458         }
    459         return last;
    460     }
    461 
    462     /**
    463      * @return The list of {@link RecipientEntry}s that have been selected by the user.
    464      */
    465     public List<RecipientEntry> getSelectedRecipients() {
    466         DrawableRecipientChip[] chips =
    467                 getText().getSpans(0, getText().length(), DrawableRecipientChip.class);
    468         List<RecipientEntry> results = new ArrayList<RecipientEntry>();
    469         if (chips == null) {
    470             return results;
    471         }
    472 
    473         for (DrawableRecipientChip c : chips) {
    474             results.add(c.getEntry());
    475         }
    476 
    477         return results;
    478     }
    479 
    480     /**
    481      * @return The list of {@link RecipientEntry}s that have been selected by the user and also
    482      *         hidden due to {@link #mMoreChip} span.
    483      */
    484     public List<RecipientEntry> getAllRecipients() {
    485         List<RecipientEntry> results = getSelectedRecipients();
    486 
    487         if (mHiddenSpans != null) {
    488             for (DrawableRecipientChip chip : mHiddenSpans) {
    489                 results.add(chip.getEntry());
    490             }
    491         }
    492 
    493         return results;
    494     }
    495 
    496     @Override
    497     public void onSelectionChanged(int start, int end) {
    498         // When selection changes, see if it is inside the chips area.
    499         // If so, move the cursor back after the chips again.
    500         // Only exception is when we change the selection due to a selected chip.
    501         DrawableRecipientChip last = getLastChip();
    502         if (mSelectedChip == null && last != null && start < getSpannable().getSpanEnd(last)) {
    503             // Grab the last chip and set the cursor to after it.
    504             setSelection(Math.min(getSpannable().getSpanEnd(last) + 1, getText().length()));
    505         }
    506         super.onSelectionChanged(start, end);
    507     }
    508 
    509     @Override
    510     public void onRestoreInstanceState(Parcelable state) {
    511         if (!TextUtils.isEmpty(getText())) {
    512             super.onRestoreInstanceState(null);
    513         } else {
    514             super.onRestoreInstanceState(state);
    515         }
    516     }
    517 
    518     @Override
    519     public Parcelable onSaveInstanceState() {
    520         // If the user changes orientation while they are editing, just roll back the selection.
    521         clearSelectedChip();
    522         return super.onSaveInstanceState();
    523     }
    524 
    525     /**
    526      * Convenience method: Append the specified text slice to the TextView's
    527      * display buffer, upgrading it to BufferType.EDITABLE if it was
    528      * not already editable. Commas are excluded as they are added automatically
    529      * by the view.
    530      */
    531     @Override
    532     public void append(CharSequence text, int start, int end) {
    533         // We don't care about watching text changes while appending.
    534         if (mTextWatcher != null) {
    535             removeTextChangedListener(mTextWatcher);
    536         }
    537         super.append(text, start, end);
    538         if (!TextUtils.isEmpty(text) && TextUtils.getTrimmedLength(text) > 0) {
    539             String displayString = text.toString();
    540 
    541             if (!displayString.trim().endsWith(String.valueOf(COMMIT_CHAR_COMMA))) {
    542                 // We have no separator, so we should add it
    543                 super.append(SEPARATOR, 0, SEPARATOR.length());
    544                 displayString += SEPARATOR;
    545             }
    546 
    547             if (!TextUtils.isEmpty(displayString)
    548                     && TextUtils.getTrimmedLength(displayString) > 0) {
    549                 mPendingChipsCount++;
    550                 mPendingChips.add(displayString);
    551             }
    552         }
    553         // Put a message on the queue to make sure we ALWAYS handle pending
    554         // chips.
    555         if (mPendingChipsCount > 0) {
    556             postHandlePendingChips();
    557         }
    558         mHandler.post(mAddTextWatcher);
    559     }
    560 
    561     @Override
    562     public void onFocusChanged(boolean hasFocus, int direction, Rect previous) {
    563         super.onFocusChanged(hasFocus, direction, previous);
    564         if (!hasFocus) {
    565             shrink();
    566         } else {
    567             expand();
    568         }
    569     }
    570 
    571     @Override
    572     public <T extends ListAdapter & Filterable> void setAdapter(@NonNull T adapter) {
    573         super.setAdapter(adapter);
    574         BaseRecipientAdapter baseAdapter = (BaseRecipientAdapter) adapter;
    575         baseAdapter.registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
    576             @Override
    577             public void onChanged(List<RecipientEntry> entries) {
    578                 int suggestionCount = entries == null ? 0 : entries.size();
    579 
    580                 // Scroll the chips field to the top of the screen so
    581                 // that the user can see as many results as possible.
    582                 if (entries != null && entries.size() > 0) {
    583                     scrollBottomIntoView();
    584                     // Here the current suggestion count is still the old one since we update
    585                     // the count at the bottom of this function.
    586                     if (mCurrentSuggestionCount == 0) {
    587                         // Announce the new number of possible choices for accessibility.
    588                         announceForAccessibilityCompat(
    589                                 getSuggestionDropdownOpenedVerbalization(suggestionCount));
    590                     }
    591                 }
    592 
    593                 // Is the dropdown closing?
    594                 if ((entries == null || entries.size() == 0)
    595                         // Here the current suggestion count is still the old one since we update
    596                         // the count at the bottom of this function.
    597                         && mCurrentSuggestionCount != 0
    598                         // If there is no text, there's no need to know if no suggestions are
    599                         // available.
    600                         && getText().length() > 0) {
    601                     announceForAccessibilityCompat(getResources().getString(
    602                             R.string.accessbility_suggestion_dropdown_closed));
    603                 }
    604 
    605                 if ((entries != null)
    606                         && (entries.size() == 1)
    607                         && (entries.get(0).getEntryType() ==
    608                                 RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST)) {
    609                     // Do nothing; showing a single permissions entry. Resizing not required.
    610                 } else {
    611                     // Set the dropdown height to be the remaining height from the anchor to the
    612                     // bottom.
    613                     mDropdownAnchor.getLocationOnScreen(mCoords);
    614                     getWindowVisibleDisplayFrame(mRect);
    615                     setDropDownHeight(mRect.bottom - mCoords[1] - mDropdownAnchor.getHeight() -
    616                             getDropDownVerticalOffset());
    617                 }
    618 
    619                 mCurrentSuggestionCount = suggestionCount;
    620             }
    621         });
    622         baseAdapter.setDropdownChipLayouter(mDropdownChipLayouter);
    623     }
    624 
    625     /**
    626      * Return the accessibility verbalization when the suggestion dropdown is opened.
    627      */
    628     public String getSuggestionDropdownOpenedVerbalization(int suggestionCount) {
    629         return getResources().getString(R.string.accessbility_suggestion_dropdown_opened);
    630     }
    631 
    632     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    633     private void announceForAccessibilityCompat(String text) {
    634         final AccessibilityManager accessibilityManager =
    635                 (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
    636         final boolean isAccessibilityOn = accessibilityManager.isEnabled();
    637 
    638         if (isAccessibilityOn && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    639             final ViewParent parent = getParent();
    640             if (parent != null) {
    641                 AccessibilityEvent event = AccessibilityEvent.obtain(
    642                         AccessibilityEvent.TYPE_ANNOUNCEMENT);
    643                 onInitializeAccessibilityEvent(event);
    644                 event.getText().add(text);
    645                 event.setContentDescription(null);
    646                 parent.requestSendAccessibilityEvent(this, event);
    647             }
    648         }
    649     }
    650 
    651     protected void scrollBottomIntoView() {
    652         if (mScrollView != null && mShouldShrink) {
    653             getLocationInWindow(mCoords);
    654             // Desired position shows at least 1 line of chips below the action
    655             // bar. We add excess padding to make sure this is always below other
    656             // content.
    657             final int height = getHeight();
    658             final int currentPos = mCoords[1] + height;
    659             mScrollView.getLocationInWindow(mCoords);
    660             final int desiredPos = mCoords[1] + height / getLineCount();
    661             if (currentPos > desiredPos) {
    662                 mScrollView.scrollBy(0, currentPos - desiredPos);
    663             }
    664         }
    665     }
    666 
    667     protected ScrollView getScrollView() {
    668         return mScrollView;
    669     }
    670 
    671     @Override
    672     public void performValidation() {
    673         // Do nothing. Chips handles its own validation.
    674     }
    675 
    676     private void shrink() {
    677         if (mTokenizer == null) {
    678             return;
    679         }
    680         long contactId = mSelectedChip != null ? mSelectedChip.getEntry().getContactId() : -1;
    681         if (mSelectedChip != null && contactId != RecipientEntry.INVALID_CONTACT
    682                 && (!isPhoneQuery() && contactId != RecipientEntry.GENERATED_CONTACT)) {
    683             clearSelectedChip();
    684         } else {
    685             if (getWidth() <= 0) {
    686                 mHandler.removeCallbacks(mDelayedShrink);
    687 
    688                 if (getVisibility() == GONE) {
    689                     // We aren't going to have a width any time soon, so defer
    690                     // this until we're not GONE.
    691                     mRequiresShrinkWhenNotGone = true;
    692                 } else {
    693                     // We don't have the width yet which means the view hasn't been drawn yet
    694                     // and there is no reason to attempt to commit chips yet.
    695                     // This focus lost must be the result of an orientation change
    696                     // or an initial rendering.
    697                     // Re-post the shrink for later.
    698                     mHandler.post(mDelayedShrink);
    699                 }
    700                 return;
    701             }
    702             // Reset any pending chips as they would have been handled
    703             // when the field lost focus.
    704             if (mPendingChipsCount > 0) {
    705                 postHandlePendingChips();
    706             } else {
    707                 Editable editable = getText();
    708                 int end = getSelectionEnd();
    709                 int start = mTokenizer.findTokenStart(editable, end);
    710                 DrawableRecipientChip[] chips =
    711                         getSpannable().getSpans(start, end, DrawableRecipientChip.class);
    712                 if ((chips == null || chips.length == 0)) {
    713                     Editable text = getText();
    714                     int whatEnd = mTokenizer.findTokenEnd(text, start);
    715                     // This token was already tokenized, so skip past the ending token.
    716                     if (whatEnd < text.length() && text.charAt(whatEnd) == ',') {
    717                         whatEnd = movePastTerminators(whatEnd);
    718                     }
    719                     // In the middle of chip; treat this as an edit
    720                     // and commit the whole token.
    721                     int selEnd = getSelectionEnd();
    722                     if (whatEnd != selEnd) {
    723                         handleEdit(start, whatEnd);
    724                     } else {
    725                         commitChip(start, end, editable);
    726                     }
    727                 }
    728             }
    729             mHandler.post(mAddTextWatcher);
    730         }
    731         createMoreChip();
    732     }
    733 
    734     private void expand() {
    735         if (mShouldShrink) {
    736             setMaxLines(Integer.MAX_VALUE);
    737         }
    738         removeMoreChip();
    739         setCursorVisible(true);
    740         Editable text = getText();
    741         setSelection(text != null && text.length() > 0 ? text.length() : 0);
    742         // If there are any temporary chips, try replacing them now that the user
    743         // has expanded the field.
    744         if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0) {
    745             new RecipientReplacementTask().execute();
    746             mTemporaryRecipients = null;
    747         }
    748     }
    749 
    750     private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) {
    751         paint.setTextSize(mChipFontSize);
    752         if (maxWidth <= 0 && Log.isLoggable(TAG, Log.DEBUG)) {
    753             Log.d(TAG, "Max width is negative: " + maxWidth);
    754         }
    755         return TextUtils.ellipsize(text, paint, maxWidth,
    756                 TextUtils.TruncateAt.END);
    757     }
    758 
    759     /**
    760      * Creates a bitmap of the given contact on a selected chip.
    761      *
    762      * @param contact The recipient entry to pull data from.
    763      * @param paint The paint to use to draw the bitmap.
    764      */
    765     private Bitmap createChipBitmap(RecipientEntry contact, TextPaint paint) {
    766         paint.setColor(getDefaultChipTextColor(contact));
    767         ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint,
    768                 getChipBackground(contact), getDefaultChipBackgroundColor(contact));
    769 
    770         if (bitmapContainer.loadIcon) {
    771             loadAvatarIcon(contact, bitmapContainer);
    772         }
    773         return bitmapContainer.bitmap;
    774     }
    775 
    776     private ChipBitmapContainer createChipBitmap(RecipientEntry contact, TextPaint paint,
    777             Drawable overrideBackgroundDrawable, int backgroundColor) {
    778         final ChipBitmapContainer result = new ChipBitmapContainer();
    779 
    780         Drawable indicatorIcon = null;
    781         int indicatorPadding = 0;
    782         if (contact.getIndicatorIconId() != 0) {
    783             indicatorIcon = getContext().getDrawable(contact.getIndicatorIconId());
    784             indicatorIcon.setBounds(0, 0,
    785                     indicatorIcon.getIntrinsicWidth(), indicatorIcon.getIntrinsicHeight());
    786             indicatorPadding = indicatorIcon.getBounds().width() + mChipTextEndPadding;
    787         }
    788 
    789         Rect backgroundPadding = new Rect();
    790         if (overrideBackgroundDrawable != null) {
    791             overrideBackgroundDrawable.getPadding(backgroundPadding);
    792         }
    793 
    794         // Ellipsize the text so that it takes AT MOST the entire width of the
    795         // autocomplete text entry area. Make sure to leave space for padding
    796         // on the sides.
    797         int height = (int) mChipHeight;
    798         // Since the icon is a square, it's width is equal to the maximum height it can be inside
    799         // the chip. Don't include iconWidth for invalid contacts and when not displaying photos.
    800         boolean displayIcon = contact.isValid() && contact.shouldDisplayIcon();
    801         int iconWidth = displayIcon ?
    802                 height - backgroundPadding.top - backgroundPadding.bottom : 0;
    803         float[] widths = new float[1];
    804         paint.getTextWidths(" ", widths);
    805         CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
    806                 calculateAvailableWidth() - iconWidth - widths[0] - backgroundPadding.left
    807                 - backgroundPadding.right - indicatorPadding);
    808         int textWidth = (int) paint.measureText(ellipsizedText, 0, ellipsizedText.length());
    809 
    810         // Chip start padding is the same as the end padding if there is no contact image.
    811         final int startPadding = displayIcon ? mChipTextStartPadding : mChipTextEndPadding;
    812         // Make sure there is a minimum chip width so the user can ALWAYS
    813         // tap a chip without difficulty.
    814         int width = Math.max(iconWidth * 2, textWidth + startPadding + mChipTextEndPadding
    815                 + iconWidth + backgroundPadding.left + backgroundPadding.right + indicatorPadding);
    816 
    817         // Create the background of the chip.
    818         result.bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    819         final Canvas canvas = new Canvas(result.bitmap);
    820 
    821         // Check if the background drawable is set via attr
    822         if (overrideBackgroundDrawable != null) {
    823             overrideBackgroundDrawable.setBounds(0, 0, width, height);
    824             overrideBackgroundDrawable.draw(canvas);
    825         } else {
    826             // Draw the default chip background
    827             mWorkPaint.reset();
    828             mWorkPaint.setColor(backgroundColor);
    829             final float radius = height / 2;
    830             canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius,
    831                     mWorkPaint);
    832         }
    833 
    834         // Draw the text vertically aligned
    835         int textX = shouldPositionAvatarOnRight() ?
    836                 mChipTextEndPadding + backgroundPadding.left + indicatorPadding :
    837                 width - backgroundPadding.right - mChipTextEndPadding - textWidth -
    838                 indicatorPadding;
    839         canvas.drawText(ellipsizedText, 0, ellipsizedText.length(),
    840                 textX, getTextYOffset(height), paint);
    841 
    842         if (indicatorIcon != null) {
    843             int indicatorX = shouldPositionAvatarOnRight()
    844                 ? backgroundPadding.left + mChipTextEndPadding
    845                 : width - backgroundPadding.right - indicatorIcon.getBounds().width()
    846                         - mChipTextEndPadding;
    847             int indicatorY = height / 2 - indicatorIcon.getBounds().height() / 2;
    848             indicatorIcon.getBounds().offsetTo(indicatorX, indicatorY);
    849             indicatorIcon.draw(canvas);
    850         }
    851 
    852         // Set the variables that are needed to draw the icon bitmap once it's loaded
    853         int iconX = shouldPositionAvatarOnRight() ? width - backgroundPadding.right - iconWidth :
    854                 backgroundPadding.left;
    855         result.left = iconX;
    856         result.top = backgroundPadding.top;
    857         result.right = iconX + iconWidth;
    858         result.bottom = height - backgroundPadding.bottom;
    859         result.loadIcon = displayIcon;
    860 
    861         return result;
    862     }
    863 
    864     /**
    865      * Helper function that draws the loaded icon bitmap into the chips bitmap
    866      */
    867     private void drawIcon(ChipBitmapContainer bitMapResult, Bitmap icon) {
    868         final Canvas canvas = new Canvas(bitMapResult.bitmap);
    869         final RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight());
    870         final RectF dst = new RectF(bitMapResult.left, bitMapResult.top, bitMapResult.right,
    871                 bitMapResult.bottom);
    872         drawIconOnCanvas(icon, canvas, src, dst);
    873     }
    874 
    875     /**
    876      * Returns true if the avatar should be positioned at the right edge of the chip.
    877      * Takes into account both the set avatar position (start or end) as well as whether
    878      * the layout direction is LTR or RTL.
    879      */
    880     private boolean shouldPositionAvatarOnRight() {
    881         final boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
    882                 getLayoutDirection() == LAYOUT_DIRECTION_RTL;
    883         final boolean assignedPosition = mAvatarPosition == AVATAR_POSITION_END;
    884         // If in Rtl mode, the position should be flipped.
    885         return isRtl ? !assignedPosition : assignedPosition;
    886     }
    887 
    888     /**
    889      * Returns the avatar icon to use for this recipient entry. Returns null if we don't want to
    890      * draw an icon for this recipient.
    891      */
    892     private void loadAvatarIcon(final RecipientEntry contact,
    893             final ChipBitmapContainer bitmapContainer) {
    894         // Don't draw photos for recipients that have been typed in OR generated on the fly.
    895         long contactId = contact.getContactId();
    896         boolean drawPhotos = isPhoneQuery() ?
    897                 contactId != RecipientEntry.INVALID_CONTACT
    898                 : (contactId != RecipientEntry.INVALID_CONTACT
    899                         && contactId != RecipientEntry.GENERATED_CONTACT);
    900 
    901         if (drawPhotos) {
    902             final byte[] origPhotoBytes = contact.getPhotoBytes();
    903             // There may not be a photo yet if anything but the first contact address
    904             // was selected.
    905             if (origPhotoBytes == null) {
    906                 // TODO: cache this in the recipient entry?
    907                 getAdapter().fetchPhoto(contact, new PhotoManager.PhotoManagerCallback() {
    908                     @Override
    909                     public void onPhotoBytesPopulated() {
    910                         // Call through to the async version which will ensure
    911                         // proper threading.
    912                         onPhotoBytesAsynchronouslyPopulated();
    913                     }
    914 
    915                     @Override
    916                     public void onPhotoBytesAsynchronouslyPopulated() {
    917                         final byte[] loadedPhotoBytes = contact.getPhotoBytes();
    918                         final Bitmap icon = BitmapFactory.decodeByteArray(loadedPhotoBytes, 0,
    919                                 loadedPhotoBytes.length);
    920                         tryDrawAndInvalidate(icon);
    921                     }
    922 
    923                     @Override
    924                     public void onPhotoBytesAsyncLoadFailed() {
    925                         // TODO: can the scaled down default photo be cached?
    926                         tryDrawAndInvalidate(mDefaultContactPhoto);
    927                     }
    928 
    929                     private void tryDrawAndInvalidate(Bitmap icon) {
    930                         drawIcon(bitmapContainer, icon);
    931                         // The caller might originated from a background task. However, if the
    932                         // background task has already completed, the view might be already drawn
    933                         // on the UI but the callback would happen on the background thread.
    934                         // So if we are on a background thread, post an invalidate call to the UI.
    935                         if (Looper.myLooper() == Looper.getMainLooper()) {
    936                             // The view might not redraw itself since it's loaded asynchronously
    937                             invalidate();
    938                         } else {
    939                             post(new Runnable() {
    940                                 @Override
    941                                 public void run() {
    942                                     invalidate();
    943                                 }
    944                             });
    945                         }
    946                     }
    947                 });
    948             } else {
    949                 final Bitmap icon = BitmapFactory.decodeByteArray(origPhotoBytes, 0,
    950                         origPhotoBytes.length);
    951                 drawIcon(bitmapContainer, icon);
    952             }
    953         }
    954     }
    955 
    956     /**
    957      * Get the background drawable for a RecipientChip.
    958      */
    959     // Visible for testing.
    960     /* package */Drawable getChipBackground(RecipientEntry contact) {
    961         return contact.isValid() ? mChipBackground : mInvalidChipBackground;
    962     }
    963 
    964     private int getDefaultChipTextColor(RecipientEntry contact) {
    965         return contact.isValid() ? mUnselectedChipTextColor :
    966                 getResources().getColor(android.R.color.black);
    967     }
    968 
    969     private int getDefaultChipBackgroundColor(RecipientEntry contact) {
    970         return contact.isValid() ? mUnselectedChipBackgroundColor :
    971                 getResources().getColor(R.color.chip_background_invalid);
    972     }
    973 
    974     /**
    975      * Given a height, returns a Y offset that will draw the text in the middle of the height.
    976      */
    977     protected float getTextYOffset(int height) {
    978         return height - ((height - mTextHeight) / 2);
    979     }
    980 
    981     /**
    982      * Draws the icon onto the canvas given the source rectangle of the bitmap and the destination
    983      * rectangle of the canvas.
    984      */
    985     protected void drawIconOnCanvas(Bitmap icon, Canvas canvas, RectF src, RectF dst) {
    986         final Matrix matrix = new Matrix();
    987 
    988         // Draw bitmap through shader first.
    989         final BitmapShader shader = new BitmapShader(icon, TileMode.CLAMP, TileMode.CLAMP);
    990         matrix.reset();
    991 
    992         // Fit bitmap to bounds.
    993         matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);
    994 
    995         shader.setLocalMatrix(matrix);
    996         mWorkPaint.reset();
    997         mWorkPaint.setShader(shader);
    998         mWorkPaint.setAntiAlias(true);
    999         mWorkPaint.setFilterBitmap(true);
   1000         mWorkPaint.setDither(true);
   1001         canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f, mWorkPaint);
   1002 
   1003         // Then draw the border.
   1004         final float borderWidth = 1f;
   1005         mWorkPaint.reset();
   1006         mWorkPaint.setColor(Color.TRANSPARENT);
   1007         mWorkPaint.setStyle(Style.STROKE);
   1008         mWorkPaint.setStrokeWidth(borderWidth);
   1009         mWorkPaint.setAntiAlias(true);
   1010         canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f - borderWidth / 2,
   1011                 mWorkPaint);
   1012 
   1013         mWorkPaint.reset();
   1014     }
   1015 
   1016     private DrawableRecipientChip constructChipSpan(RecipientEntry contact) {
   1017         TextPaint paint = getPaint();
   1018         float defaultSize = paint.getTextSize();
   1019         int defaultColor = paint.getColor();
   1020 
   1021         Bitmap tmpBitmap = createChipBitmap(contact, paint);
   1022 
   1023         // Pass the full text, un-ellipsized, to the chip.
   1024         Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
   1025         result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
   1026         VisibleRecipientChip recipientChip =
   1027                 new VisibleRecipientChip(result, contact);
   1028         recipientChip.setExtraMargin(mLineSpacingExtra);
   1029         // Return text to the original size.
   1030         paint.setTextSize(defaultSize);
   1031         paint.setColor(defaultColor);
   1032         return recipientChip;
   1033     }
   1034 
   1035     /**
   1036      * Calculate the offset from bottom of the EditText to top of the provided line.
   1037      */
   1038     private int calculateOffsetFromBottomToTop(int line) {
   1039         return -(int) ((mChipHeight + (2 * mLineSpacingExtra)) * (Math
   1040                 .abs(getLineCount() - line)) + getPaddingBottom());
   1041     }
   1042 
   1043     /**
   1044      * Get the max amount of space a chip can take up. The formula takes into
   1045      * account the width of the EditTextView, any view padding, and padding
   1046      * that will be added to the chip.
   1047      */
   1048     private float calculateAvailableWidth() {
   1049         return getWidth() - getPaddingLeft() - getPaddingRight() - mChipTextStartPadding
   1050                 - mChipTextEndPadding;
   1051     }
   1052 
   1053 
   1054     private void setChipDimensions(Context context, AttributeSet attrs) {
   1055         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecipientEditTextView, 0,
   1056                 0);
   1057         Resources r = getContext().getResources();
   1058 
   1059         mChipBackground = a.getDrawable(R.styleable.RecipientEditTextView_chipBackground);
   1060         mInvalidChipBackground = a
   1061                 .getDrawable(R.styleable.RecipientEditTextView_invalidChipBackground);
   1062         mChipDelete = a.getDrawable(R.styleable.RecipientEditTextView_chipDelete);
   1063         if (mChipDelete == null) {
   1064             mChipDelete = r.getDrawable(R.drawable.ic_cancel_wht_24dp);
   1065         }
   1066         mChipTextStartPadding = mChipTextEndPadding
   1067                 = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipPadding, -1);
   1068         if (mChipTextStartPadding == -1) {
   1069             mChipTextStartPadding = mChipTextEndPadding =
   1070                     (int) r.getDimension(R.dimen.chip_padding);
   1071         }
   1072         // xml-overrides for each individual padding
   1073         // TODO: add these to attr?
   1074         int overridePadding = (int) r.getDimension(R.dimen.chip_padding_start);
   1075         if (overridePadding >= 0) {
   1076             mChipTextStartPadding = overridePadding;
   1077         }
   1078         overridePadding = (int) r.getDimension(R.dimen.chip_padding_end);
   1079         if (overridePadding >= 0) {
   1080             mChipTextEndPadding = overridePadding;
   1081         }
   1082 
   1083         mDefaultContactPhoto = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture);
   1084 
   1085         mMoreItem = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.more_item, null);
   1086 
   1087         mChipHeight = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipHeight, -1);
   1088         if (mChipHeight == -1) {
   1089             mChipHeight = r.getDimension(R.dimen.chip_height);
   1090         }
   1091         mChipFontSize = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipFontSize, -1);
   1092         if (mChipFontSize == -1) {
   1093             mChipFontSize = r.getDimension(R.dimen.chip_text_size);
   1094         }
   1095         mAvatarPosition =
   1096                 a.getInt(R.styleable.RecipientEditTextView_avatarPosition, AVATAR_POSITION_START);
   1097         mDisableDelete = a.getBoolean(R.styleable.RecipientEditTextView_disableDelete, false);
   1098 
   1099         mMaxLines = r.getInteger(R.integer.chips_max_lines);
   1100         mLineSpacingExtra = r.getDimensionPixelOffset(R.dimen.line_spacing_extra);
   1101 
   1102         mUnselectedChipTextColor = a.getColor(
   1103                 R.styleable.RecipientEditTextView_unselectedChipTextColor,
   1104                 r.getColor(android.R.color.black));
   1105 
   1106         mUnselectedChipBackgroundColor = a.getColor(
   1107                 R.styleable.RecipientEditTextView_unselectedChipBackgroundColor,
   1108                 r.getColor(R.color.chip_background));
   1109 
   1110         a.recycle();
   1111     }
   1112 
   1113     // Visible for testing.
   1114     /* package */ void setMoreItem(TextView moreItem) {
   1115         mMoreItem = moreItem;
   1116     }
   1117 
   1118 
   1119     // Visible for testing.
   1120     /* package */ void setChipBackground(Drawable chipBackground) {
   1121         mChipBackground = chipBackground;
   1122     }
   1123 
   1124     // Visible for testing.
   1125     /* package */ void setChipHeight(int height) {
   1126         mChipHeight = height;
   1127     }
   1128 
   1129     public float getChipHeight() {
   1130         return mChipHeight;
   1131     }
   1132 
   1133     /** Returns whether view is in no-chip or chip mode. */
   1134     public boolean isNoChipMode() {
   1135         return mNoChipMode;
   1136     }
   1137 
   1138     /**
   1139      * Set whether to shrink the recipients field such that at most
   1140      * one line of recipients chips are shown when the field loses
   1141      * focus. By default, the number of displayed recipients will be
   1142      * limited and a "more" chip will be shown when focus is lost.
   1143      * @param shrink
   1144      */
   1145     public void setOnFocusListShrinkRecipients(boolean shrink) {
   1146         mShouldShrink = shrink;
   1147     }
   1148 
   1149     @Override
   1150     public void onSizeChanged(int width, int height, int oldw, int oldh) {
   1151         super.onSizeChanged(width, height, oldw, oldh);
   1152         if (width != 0 && height != 0) {
   1153             if (mPendingChipsCount > 0) {
   1154                 postHandlePendingChips();
   1155             } else {
   1156                 checkChipWidths();
   1157             }
   1158         }
   1159         // Try to find the scroll view parent, if it exists.
   1160         if (mScrollView == null && !mTriedGettingScrollView) {
   1161             ViewParent parent = getParent();
   1162             while (parent != null && !(parent instanceof ScrollView)) {
   1163                 parent = parent.getParent();
   1164             }
   1165             if (parent != null) {
   1166                 mScrollView = (ScrollView) parent;
   1167             }
   1168             mTriedGettingScrollView = true;
   1169         }
   1170     }
   1171 
   1172     private void postHandlePendingChips() {
   1173         mHandler.removeCallbacks(mHandlePendingChips);
   1174         mHandler.post(mHandlePendingChips);
   1175     }
   1176 
   1177     private void checkChipWidths() {
   1178         // Check the widths of the associated chips.
   1179         DrawableRecipientChip[] chips = getSortedRecipients();
   1180         if (chips != null) {
   1181             Rect bounds;
   1182             for (DrawableRecipientChip chip : chips) {
   1183                 bounds = chip.getBounds();
   1184                 if (getWidth() > 0 && bounds.right - bounds.left >
   1185                         getWidth() - getPaddingLeft() - getPaddingRight()) {
   1186                     // Need to redraw that chip.
   1187                     replaceChip(chip, chip.getEntry());
   1188                 }
   1189             }
   1190         }
   1191     }
   1192 
   1193     // Visible for testing.
   1194     /*package*/ void handlePendingChips() {
   1195         if (getViewWidth() <= 0) {
   1196             // The widget has not been sized yet.
   1197             // This will be called as a result of onSizeChanged
   1198             // at a later point.
   1199             return;
   1200         }
   1201         if (mPendingChipsCount <= 0) {
   1202             return;
   1203         }
   1204 
   1205         synchronized (mPendingChips) {
   1206             Editable editable = getText();
   1207             // Tokenize!
   1208             if (mPendingChipsCount <= MAX_CHIPS_PARSED) {
   1209                 for (int i = 0; i < mPendingChips.size(); i++) {
   1210                     String current = mPendingChips.get(i);
   1211                     int tokenStart = editable.toString().indexOf(current);
   1212                     // Always leave a space at the end between tokens.
   1213                     int tokenEnd = tokenStart + current.length() - 1;
   1214                     if (tokenStart >= 0) {
   1215                         // When we have a valid token, include it with the token
   1216                         // to the left.
   1217                         if (tokenEnd < editable.length() - 2
   1218                                 && editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) {
   1219                             tokenEnd++;
   1220                         }
   1221                         createReplacementChip(tokenStart, tokenEnd, editable, i < CHIP_LIMIT
   1222                                 || !mShouldShrink);
   1223                     }
   1224                     mPendingChipsCount--;
   1225                 }
   1226                 sanitizeEnd();
   1227             } else {
   1228                 mNoChipMode = true;
   1229             }
   1230 
   1231             if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0
   1232                     && mTemporaryRecipients.size() <= RecipientAlternatesAdapter.MAX_LOOKUPS) {
   1233                 if (hasFocus() || mTemporaryRecipients.size() < CHIP_LIMIT) {
   1234                     new RecipientReplacementTask().execute();
   1235                     mTemporaryRecipients = null;
   1236                 } else {
   1237                     // Create the "more" chip
   1238                     mIndividualReplacements = new IndividualReplacementTask();
   1239                     mIndividualReplacements.execute(new ArrayList<DrawableRecipientChip>(
   1240                             mTemporaryRecipients.subList(0, CHIP_LIMIT)));
   1241                     if (mTemporaryRecipients.size() > CHIP_LIMIT) {
   1242                         mTemporaryRecipients = new ArrayList<DrawableRecipientChip>(
   1243                                 mTemporaryRecipients.subList(CHIP_LIMIT,
   1244                                         mTemporaryRecipients.size()));
   1245                     } else {
   1246                         mTemporaryRecipients = null;
   1247                     }
   1248                     createMoreChip();
   1249                 }
   1250             } else {
   1251                 // There are too many recipients to look up, so just fall back
   1252                 // to showing addresses for all of them.
   1253                 mTemporaryRecipients = null;
   1254                 createMoreChip();
   1255             }
   1256             mPendingChipsCount = 0;
   1257             mPendingChips.clear();
   1258         }
   1259     }
   1260 
   1261     // Visible for testing.
   1262     /*package*/ int getViewWidth() {
   1263         return getWidth();
   1264     }
   1265 
   1266     /**
   1267      * Remove any characters after the last valid chip.
   1268      */
   1269     // Visible for testing.
   1270     /*package*/ void sanitizeEnd() {
   1271         // Don't sanitize while we are waiting for pending chips to complete.
   1272         if (mPendingChipsCount > 0) {
   1273             return;
   1274         }
   1275         // Find the last chip; eliminate any commit characters after it.
   1276         DrawableRecipientChip[] chips = getSortedRecipients();
   1277         Spannable spannable = getSpannable();
   1278         if (chips != null && chips.length > 0) {
   1279             int end;
   1280             mMoreChip = getMoreChip();
   1281             if (mMoreChip != null) {
   1282                 end = spannable.getSpanEnd(mMoreChip);
   1283             } else {
   1284                 end = getSpannable().getSpanEnd(getLastChip());
   1285             }
   1286             Editable editable = getText();
   1287             int length = editable.length();
   1288             if (length > end) {
   1289                 // See what characters occur after that and eliminate them.
   1290                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   1291                     Log.d(TAG, "There were extra characters after the last tokenizable entry."
   1292                             + editable);
   1293                 }
   1294                 editable.delete(end + 1, length);
   1295             }
   1296         }
   1297     }
   1298 
   1299     /**
   1300      * Create a chip that represents just the email address of a recipient. At some later
   1301      * point, this chip will be attached to a real contact entry, if one exists.
   1302      */
   1303     // VisibleForTesting
   1304     void createReplacementChip(int tokenStart, int tokenEnd, Editable editable,
   1305             boolean visible) {
   1306         if (alreadyHasChip(tokenStart, tokenEnd)) {
   1307             // There is already a chip present at this location.
   1308             // Don't recreate it.
   1309             return;
   1310         }
   1311         String token = editable.toString().substring(tokenStart, tokenEnd);
   1312         final String trimmedToken = token.trim();
   1313         int commitCharIndex = trimmedToken.lastIndexOf(COMMIT_CHAR_COMMA);
   1314         if (commitCharIndex != -1 && commitCharIndex == trimmedToken.length() - 1) {
   1315             token = trimmedToken.substring(0, trimmedToken.length() - 1);
   1316         }
   1317         RecipientEntry entry = createTokenizedEntry(token);
   1318         if (entry != null) {
   1319             DrawableRecipientChip chip = null;
   1320             try {
   1321                 if (!mNoChipMode) {
   1322                     chip = visible ? constructChipSpan(entry) : new InvisibleRecipientChip(entry);
   1323                 }
   1324             } catch (NullPointerException e) {
   1325                 Log.e(TAG, e.getMessage(), e);
   1326             }
   1327             editable.setSpan(chip, tokenStart, tokenEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1328             // Add this chip to the list of entries "to replace"
   1329             if (chip != null) {
   1330                 if (mTemporaryRecipients == null) {
   1331                     mTemporaryRecipients = new ArrayList<DrawableRecipientChip>();
   1332                 }
   1333                 chip.setOriginalText(token);
   1334                 mTemporaryRecipients.add(chip);
   1335             }
   1336         }
   1337     }
   1338 
   1339     // VisibleForTesting
   1340     RecipientEntry createTokenizedEntry(final String token) {
   1341         if (TextUtils.isEmpty(token)) {
   1342             return null;
   1343         }
   1344         if (isPhoneQuery() && PhoneUtil.isPhoneNumber(token)) {
   1345             return RecipientEntry.constructFakePhoneEntry(token, true);
   1346         }
   1347         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token);
   1348         boolean isValid = isValid(token);
   1349         if (isValid && tokens != null && tokens.length > 0) {
   1350             // If we can get a name from tokenizing, then generate an entry from
   1351             // this.
   1352             String display = tokens[0].getName();
   1353             if (!TextUtils.isEmpty(display)) {
   1354                 return RecipientEntry.constructGeneratedEntry(display, tokens[0].getAddress(),
   1355                         isValid);
   1356             } else {
   1357                 display = tokens[0].getAddress();
   1358                 if (!TextUtils.isEmpty(display)) {
   1359                     return RecipientEntry.constructFakeEntry(display, isValid);
   1360                 }
   1361             }
   1362         }
   1363         // Unable to validate the token or to create a valid token from it.
   1364         // Just create a chip the user can edit.
   1365         String validatedToken = null;
   1366         if (mValidator != null && !isValid) {
   1367             // Try fixing up the entry using the validator.
   1368             validatedToken = mValidator.fixText(token).toString();
   1369             if (!TextUtils.isEmpty(validatedToken)) {
   1370                 if (validatedToken.contains(token)) {
   1371                     // protect against the case of a validator with a null
   1372                     // domain,
   1373                     // which doesn't add a domain to the token
   1374                     Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(validatedToken);
   1375                     if (tokenized.length > 0) {
   1376                         validatedToken = tokenized[0].getAddress();
   1377                         isValid = true;
   1378                     }
   1379                 } else {
   1380                     // We ran into a case where the token was invalid and
   1381                     // removed
   1382                     // by the validator. In this case, just use the original
   1383                     // token
   1384                     // and let the user sort out the error chip.
   1385                     validatedToken = null;
   1386                     isValid = false;
   1387                 }
   1388             }
   1389         }
   1390         // Otherwise, fallback to just creating an editable email address chip.
   1391         return RecipientEntry.constructFakeEntry(
   1392                 !TextUtils.isEmpty(validatedToken) ? validatedToken : token, isValid);
   1393     }
   1394 
   1395     private boolean isValid(String text) {
   1396         return mValidator == null ? true : mValidator.isValid(text);
   1397     }
   1398 
   1399     private static String tokenizeAddress(String destination) {
   1400         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination);
   1401         if (tokens != null && tokens.length > 0) {
   1402             return tokens[0].getAddress();
   1403         }
   1404         return destination;
   1405     }
   1406 
   1407     @Override
   1408     public void setTokenizer(Tokenizer tokenizer) {
   1409         mTokenizer = tokenizer;
   1410         super.setTokenizer(mTokenizer);
   1411     }
   1412 
   1413     @Override
   1414     public void setValidator(Validator validator) {
   1415         mValidator = validator;
   1416         super.setValidator(validator);
   1417     }
   1418 
   1419     /**
   1420      * We cannot use the default mechanism for replaceText. Instead,
   1421      * we override onItemClickListener so we can get all the associated
   1422      * contact information including display text, address, and id.
   1423      */
   1424     @Override
   1425     protected void replaceText(CharSequence text) {
   1426         return;
   1427     }
   1428 
   1429     /**
   1430      * Dismiss any selected chips when the back key is pressed.
   1431      */
   1432     @Override
   1433     public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
   1434         if (keyCode == KeyEvent.KEYCODE_BACK && mSelectedChip != null) {
   1435             clearSelectedChip();
   1436             return true;
   1437         }
   1438         return super.onKeyPreIme(keyCode, event);
   1439     }
   1440 
   1441     /**
   1442      * Monitor key presses in this view to see if the user types
   1443      * any commit keys, which consist of ENTER, TAB, or DPAD_CENTER.
   1444      * If the user has entered text that has contact matches and types
   1445      * a commit key, create a chip from the topmost matching contact.
   1446      * If the user has entered text that has no contact matches and types
   1447      * a commit key, then create a chip from the text they have entered.
   1448      */
   1449     @Override
   1450     public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
   1451         switch (keyCode) {
   1452             case KeyEvent.KEYCODE_TAB:
   1453                 if (event.hasNoModifiers()) {
   1454                     if (mSelectedChip != null) {
   1455                         clearSelectedChip();
   1456                     } else {
   1457                         commitDefault();
   1458                     }
   1459                 }
   1460                 break;
   1461         }
   1462         return super.onKeyUp(keyCode, event);
   1463     }
   1464 
   1465     private boolean focusNext() {
   1466         View next = focusSearch(View.FOCUS_DOWN);
   1467         if (next != null) {
   1468             next.requestFocus();
   1469             return true;
   1470         }
   1471         return false;
   1472     }
   1473 
   1474     /**
   1475      * Create a chip from the default selection. If the popup is showing, the
   1476      * default is the selected item (if one is selected), or the first item, in the popup
   1477      * suggestions list. Otherwise, it is whatever the user had typed in. End represents where the
   1478      * tokenizer should search for a token to turn into a chip.
   1479      * @return If a chip was created from a real contact.
   1480      */
   1481     private boolean commitDefault() {
   1482         // If there is no tokenizer, don't try to commit.
   1483         if (mTokenizer == null) {
   1484             return false;
   1485         }
   1486         Editable editable = getText();
   1487         int end = getSelectionEnd();
   1488         int start = mTokenizer.findTokenStart(editable, end);
   1489 
   1490         if (shouldCreateChip(start, end)) {
   1491             int whatEnd = mTokenizer.findTokenEnd(getText(), start);
   1492             // In the middle of chip; treat this as an edit
   1493             // and commit the whole token.
   1494             whatEnd = movePastTerminators(whatEnd);
   1495             if (whatEnd != getSelectionEnd()) {
   1496                 handleEdit(start, whatEnd);
   1497                 return true;
   1498             }
   1499             return commitChip(start, end , editable);
   1500         }
   1501         return false;
   1502     }
   1503 
   1504     private void commitByCharacter() {
   1505         // We can't possibly commit by character if we can't tokenize.
   1506         if (mTokenizer == null) {
   1507             return;
   1508         }
   1509         Editable editable = getText();
   1510         int end = getSelectionEnd();
   1511         int start = mTokenizer.findTokenStart(editable, end);
   1512         if (shouldCreateChip(start, end)) {
   1513             commitChip(start, end, editable);
   1514         }
   1515         setSelection(getText().length());
   1516     }
   1517 
   1518     private boolean commitChip(int start, int end, Editable editable) {
   1519         int position = positionOfFirstEntryWithTypePerson();
   1520         if (position != -1 && enoughToFilter()
   1521                 && end == getSelectionEnd() && !isPhoneQuery()
   1522                 && !isValidEmailAddress(editable.toString().substring(start, end).trim())) {
   1523             // let's choose the selected or first entry if only the input text is NOT an email
   1524             // address so we won't try to replace the user's potentially correct but
   1525             // new/unencountered email input
   1526             final int selectedPosition = getListSelection();
   1527             if (selectedPosition == -1 || !isEntryAtPositionTypePerson(selectedPosition)) {
   1528                 // Nothing is selected or selected item is not type person; use the first item
   1529                 submitItemAtPosition(position);
   1530             } else {
   1531                 submitItemAtPosition(selectedPosition);
   1532             }
   1533             dismissDropDown();
   1534             return true;
   1535         } else {
   1536             int tokenEnd = mTokenizer.findTokenEnd(editable, start);
   1537             if (editable.length() > tokenEnd + 1) {
   1538                 char charAt = editable.charAt(tokenEnd + 1);
   1539                 if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) {
   1540                     tokenEnd++;
   1541                 }
   1542             }
   1543             String text = editable.toString().substring(start, tokenEnd).trim();
   1544             clearComposingText();
   1545             if (text.length() > 0 && !text.equals(" ")) {
   1546                 RecipientEntry entry = createTokenizedEntry(text);
   1547                 if (entry != null) {
   1548                     QwertyKeyListener.markAsReplaced(editable, start, end, "");
   1549                     CharSequence chipText = createChip(entry);
   1550                     if (chipText != null && start > -1 && end > -1) {
   1551                         editable.replace(start, end, chipText);
   1552                     }
   1553                 }
   1554                 // Only dismiss the dropdown if it is related to the text we
   1555                 // just committed.
   1556                 // For paste, it may not be as there are possibly multiple
   1557                 // tokens being added.
   1558                 if (end == getSelectionEnd()) {
   1559                     dismissDropDown();
   1560                 }
   1561                 sanitizeBetween();
   1562                 return true;
   1563             }
   1564         }
   1565         return false;
   1566     }
   1567 
   1568     private int positionOfFirstEntryWithTypePerson() {
   1569         ListAdapter adapter = getAdapter();
   1570         int itemCount = adapter != null ? adapter.getCount() : 0;
   1571         for (int i = 0; i < itemCount; i++) {
   1572             if (isEntryAtPositionTypePerson(i)) {
   1573                 return i;
   1574             }
   1575         }
   1576         return -1;
   1577     }
   1578 
   1579     private boolean isEntryAtPositionTypePerson(int position) {
   1580         return getAdapter().getItem(position).getEntryType() == RecipientEntry.ENTRY_TYPE_PERSON;
   1581     }
   1582 
   1583     // Visible for testing.
   1584     /* package */ void sanitizeBetween() {
   1585         // Don't sanitize while we are waiting for content to chipify.
   1586         if (mPendingChipsCount > 0) {
   1587             return;
   1588         }
   1589         // Find the last chip.
   1590         DrawableRecipientChip[] recips = getSortedRecipients();
   1591         if (recips != null && recips.length > 0) {
   1592             DrawableRecipientChip last = recips[recips.length - 1];
   1593             DrawableRecipientChip beforeLast = null;
   1594             if (recips.length > 1) {
   1595                 beforeLast = recips[recips.length - 2];
   1596             }
   1597             int startLooking = 0;
   1598             int end = getSpannable().getSpanStart(last);
   1599             if (beforeLast != null) {
   1600                 startLooking = getSpannable().getSpanEnd(beforeLast);
   1601                 Editable text = getText();
   1602                 if (startLooking == -1 || startLooking > text.length() - 1) {
   1603                     // There is nothing after this chip.
   1604                     return;
   1605                 }
   1606                 if (text.charAt(startLooking) == ' ') {
   1607                     startLooking++;
   1608                 }
   1609             }
   1610             if (startLooking >= 0 && end >= 0 && startLooking < end) {
   1611                 getText().delete(startLooking, end);
   1612             }
   1613         }
   1614     }
   1615 
   1616     private boolean shouldCreateChip(int start, int end) {
   1617         return !mNoChipMode && hasFocus() && enoughToFilter() && !alreadyHasChip(start, end);
   1618     }
   1619 
   1620     private boolean alreadyHasChip(int start, int end) {
   1621         if (mNoChipMode) {
   1622             return true;
   1623         }
   1624         DrawableRecipientChip[] chips =
   1625                 getSpannable().getSpans(start, end, DrawableRecipientChip.class);
   1626         return chips != null && chips.length > 0;
   1627     }
   1628 
   1629     private void handleEdit(int start, int end) {
   1630         if (start == -1 || end == -1) {
   1631             // This chip no longer exists in the field.
   1632             dismissDropDown();
   1633             return;
   1634         }
   1635         // This is in the middle of a chip, so select out the whole chip
   1636         // and commit it.
   1637         Editable editable = getText();
   1638         setSelection(end);
   1639         String text = getText().toString().substring(start, end);
   1640         if (!TextUtils.isEmpty(text)) {
   1641             RecipientEntry entry = RecipientEntry.constructFakeEntry(text, isValid(text));
   1642             QwertyKeyListener.markAsReplaced(editable, start, end, "");
   1643             CharSequence chipText = createChip(entry);
   1644             int selEnd = getSelectionEnd();
   1645             if (chipText != null && start > -1 && selEnd > -1) {
   1646                 editable.replace(start, selEnd, chipText);
   1647             }
   1648         }
   1649         dismissDropDown();
   1650     }
   1651 
   1652     /**
   1653      * If there is a selected chip, delegate the key events
   1654      * to the selected chip.
   1655      */
   1656     @Override
   1657     public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
   1658         if (mSelectedChip != null && keyCode == KeyEvent.KEYCODE_DEL) {
   1659             if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
   1660                 mAlternatesPopup.dismiss();
   1661             }
   1662             removeChip(mSelectedChip);
   1663         }
   1664 
   1665         switch (keyCode) {
   1666             case KeyEvent.KEYCODE_ENTER:
   1667             case KeyEvent.KEYCODE_DPAD_CENTER:
   1668                 if (event.hasNoModifiers()) {
   1669                     if (commitDefault()) {
   1670                         return true;
   1671                     }
   1672                     if (mSelectedChip != null) {
   1673                         clearSelectedChip();
   1674                         return true;
   1675                     } else if (focusNext()) {
   1676                         return true;
   1677                     }
   1678                 }
   1679                 break;
   1680         }
   1681 
   1682         return super.onKeyDown(keyCode, event);
   1683     }
   1684 
   1685     // Visible for testing.
   1686     /* package */ Spannable getSpannable() {
   1687         return getText();
   1688     }
   1689 
   1690     private int getChipStart(DrawableRecipientChip chip) {
   1691         return getSpannable().getSpanStart(chip);
   1692     }
   1693 
   1694     private int getChipEnd(DrawableRecipientChip chip) {
   1695         return getSpannable().getSpanEnd(chip);
   1696     }
   1697 
   1698     /**
   1699      * Instead of filtering on the entire contents of the edit box,
   1700      * this subclass method filters on the range from
   1701      * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
   1702      * if the length of that range meets or exceeds {@link #getThreshold}
   1703      * and makes sure that the range is not already a Chip.
   1704      */
   1705     @Override
   1706     public void performFiltering(@NonNull CharSequence text, int keyCode) {
   1707         boolean isCompletedToken = isCompletedToken(text);
   1708         if (enoughToFilter() && !isCompletedToken) {
   1709             int end = getSelectionEnd();
   1710             int start = mTokenizer.findTokenStart(text, end);
   1711             // If this is a RecipientChip, don't filter
   1712             // on its contents.
   1713             Spannable span = getSpannable();
   1714             DrawableRecipientChip[] chips = span.getSpans(start, end, DrawableRecipientChip.class);
   1715             if (chips != null && chips.length > 0) {
   1716                 dismissDropDown();
   1717                 return;
   1718             }
   1719         } else if (isCompletedToken) {
   1720             dismissDropDown();
   1721             return;
   1722         }
   1723         super.performFiltering(text, keyCode);
   1724     }
   1725 
   1726     // Visible for testing.
   1727     /*package*/ boolean isCompletedToken(CharSequence text) {
   1728         if (TextUtils.isEmpty(text)) {
   1729             return false;
   1730         }
   1731         // Check to see if this is a completed token before filtering.
   1732         int end = text.length();
   1733         int start = mTokenizer.findTokenStart(text, end);
   1734         String token = text.toString().substring(start, end).trim();
   1735         if (!TextUtils.isEmpty(token)) {
   1736             char atEnd = token.charAt(token.length() - 1);
   1737             return atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON;
   1738         }
   1739         return false;
   1740     }
   1741 
   1742     /**
   1743      * Clears the selected chip if there is one (and dismissing any popups related to the selected
   1744      * chip in the process).
   1745      */
   1746     public void clearSelectedChip() {
   1747         if (mSelectedChip != null) {
   1748             unselectChip(mSelectedChip);
   1749             mSelectedChip = null;
   1750         }
   1751         setCursorVisible(true);
   1752         setSelection(getText().length());
   1753     }
   1754 
   1755     /**
   1756      * Monitor touch events in the RecipientEditTextView.
   1757      * If the view does not have focus, any tap on the view
   1758      * will just focus the view. If the view has focus, determine
   1759      * if the touch target is a recipient chip. If it is and the chip
   1760      * is not selected, select it and clear any other selected chips.
   1761      * If it isn't, then select that chip.
   1762      */
   1763     @Override
   1764     public boolean onTouchEvent(@NonNull MotionEvent event) {
   1765         if (!isFocused()) {
   1766             // Ignore any chip taps until this view is focused.
   1767             return super.onTouchEvent(event);
   1768         }
   1769         boolean handled = super.onTouchEvent(event);
   1770         int action = event.getAction();
   1771         boolean chipWasSelected = false;
   1772         if (mSelectedChip == null) {
   1773             mGestureDetector.onTouchEvent(event);
   1774         }
   1775         if (action == MotionEvent.ACTION_UP) {
   1776             float x = event.getX();
   1777             float y = event.getY();
   1778             int offset = putOffsetInRange(x, y);
   1779             DrawableRecipientChip currentChip = findChip(offset);
   1780             if (currentChip != null) {
   1781                 if (mSelectedChip != null && mSelectedChip != currentChip) {
   1782                     clearSelectedChip();
   1783                     selectChip(currentChip);
   1784                 } else if (mSelectedChip == null) {
   1785                     commitDefault();
   1786                     selectChip(currentChip);
   1787                 } else {
   1788                     onClick(mSelectedChip);
   1789                 }
   1790                 chipWasSelected = true;
   1791                 handled = true;
   1792             } else if (mSelectedChip != null && shouldShowEditableText(mSelectedChip)) {
   1793                 chipWasSelected = true;
   1794             }
   1795         }
   1796         if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
   1797             clearSelectedChip();
   1798         }
   1799         return handled;
   1800     }
   1801 
   1802     private void showAlternates(final DrawableRecipientChip currentChip,
   1803             final ListPopupWindow alternatesPopup) {
   1804         new AsyncTask<Void, Void, ListAdapter>() {
   1805             @Override
   1806             protected ListAdapter doInBackground(final Void... params) {
   1807                 return createAlternatesAdapter(currentChip);
   1808             }
   1809 
   1810             @Override
   1811             protected void onPostExecute(final ListAdapter result) {
   1812                 if (!mAttachedToWindow) {
   1813                     return;
   1814                 }
   1815                 int line = getLayout().getLineForOffset(getChipStart(currentChip));
   1816                 int bottomOffset = calculateOffsetFromBottomToTop(line);
   1817 
   1818                 // Align the alternates popup with the left side of the View,
   1819                 // regardless of the position of the chip tapped.
   1820                 alternatesPopup.setAnchorView((mAlternatePopupAnchor != null) ?
   1821                         mAlternatePopupAnchor : RecipientEditTextView.this);
   1822                 alternatesPopup.setVerticalOffset(bottomOffset);
   1823                 alternatesPopup.setAdapter(result);
   1824                 alternatesPopup.setOnItemClickListener(mAlternatesListener);
   1825                 // Clear the checked item.
   1826                 mCheckedItem = -1;
   1827                 alternatesPopup.show();
   1828                 ListView listView = alternatesPopup.getListView();
   1829                 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
   1830                 // Checked item would be -1 if the adapter has not
   1831                 // loaded the view that should be checked yet. The
   1832                 // variable will be set correctly when onCheckedItemChanged
   1833                 // is called in a separate thread.
   1834                 if (mCheckedItem != -1) {
   1835                     listView.setItemChecked(mCheckedItem, true);
   1836                     mCheckedItem = -1;
   1837                 }
   1838             }
   1839         }.execute((Void[]) null);
   1840     }
   1841 
   1842     protected ListAdapter createAlternatesAdapter(DrawableRecipientChip chip) {
   1843         return new RecipientAlternatesAdapter(getContext(), chip.getContactId(),
   1844                 chip.getDirectoryId(), chip.getLookupKey(), chip.getDataId(),
   1845                 getAdapter().getQueryType(), this, mDropdownChipLayouter,
   1846                 constructStateListDeleteDrawable(), getAdapter().getPermissionsCheckListener());
   1847     }
   1848 
   1849     private ListAdapter createSingleAddressAdapter(DrawableRecipientChip currentChip) {
   1850         return new SingleRecipientArrayAdapter(getContext(), currentChip.getEntry(),
   1851                 mDropdownChipLayouter, constructStateListDeleteDrawable());
   1852     }
   1853 
   1854     private StateListDrawable constructStateListDeleteDrawable() {
   1855         // Construct the StateListDrawable from deleteDrawable
   1856         StateListDrawable deleteDrawable = new StateListDrawable();
   1857         if (!mDisableDelete) {
   1858             deleteDrawable.addState(new int[]{android.R.attr.state_activated}, mChipDelete);
   1859         }
   1860         deleteDrawable.addState(new int[0], null);
   1861         return deleteDrawable;
   1862     }
   1863 
   1864     @Override
   1865     public void onCheckedItemChanged(int position) {
   1866         ListView listView = mAlternatesPopup.getListView();
   1867         if (listView != null && listView.getCheckedItemCount() == 0) {
   1868             listView.setItemChecked(position, true);
   1869         }
   1870         mCheckedItem = position;
   1871     }
   1872 
   1873     private int putOffsetInRange(final float x, final float y) {
   1874         final int offset;
   1875 
   1876         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
   1877             offset = getOffsetForPosition(x, y);
   1878         } else {
   1879             offset = supportGetOffsetForPosition(x, y);
   1880         }
   1881 
   1882         return putOffsetInRange(offset);
   1883     }
   1884 
   1885     // TODO: This algorithm will need a lot of tweaking after more people have used
   1886     // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
   1887     // what comes before the finger.
   1888     private int putOffsetInRange(int o) {
   1889         int offset = o;
   1890         Editable text = getText();
   1891         int length = text.length();
   1892         // Remove whitespace from end to find "real end"
   1893         int realLength = length;
   1894         for (int i = length - 1; i >= 0; i--) {
   1895             if (text.charAt(i) == ' ') {
   1896                 realLength--;
   1897             } else {
   1898                 break;
   1899             }
   1900         }
   1901 
   1902         // If the offset is beyond or at the end of the text,
   1903         // leave it alone.
   1904         if (offset >= realLength) {
   1905             return offset;
   1906         }
   1907         Editable editable = getText();
   1908         while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
   1909             // Keep walking backward!
   1910             offset--;
   1911         }
   1912         return offset;
   1913     }
   1914 
   1915     private static int findText(Editable text, int offset) {
   1916         if (text.charAt(offset) != ' ') {
   1917             return offset;
   1918         }
   1919         return -1;
   1920     }
   1921 
   1922     private DrawableRecipientChip findChip(int offset) {
   1923         final Spannable span = getSpannable();
   1924         final DrawableRecipientChip[] chips =
   1925                 span.getSpans(0, span.length(), DrawableRecipientChip.class);
   1926         // Find the chip that contains this offset.
   1927         for (DrawableRecipientChip chip : chips) {
   1928             int start = getChipStart(chip);
   1929             int end = getChipEnd(chip);
   1930             if (offset >= start && offset <= end) {
   1931                 return chip;
   1932             }
   1933         }
   1934         return null;
   1935     }
   1936 
   1937     // Visible for testing.
   1938     // Use this method to generate text to add to the list of addresses.
   1939     /* package */String createAddressText(RecipientEntry entry) {
   1940         String display = entry.getDisplayName();
   1941         String address = entry.getDestination();
   1942         if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
   1943             display = null;
   1944         }
   1945         String trimmedDisplayText;
   1946         if (isPhoneQuery() && PhoneUtil.isPhoneNumber(address)) {
   1947             trimmedDisplayText = address.trim();
   1948         } else {
   1949             if (address != null) {
   1950                 // Tokenize out the address in case the address already
   1951                 // contained the username as well.
   1952                 Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(address);
   1953                 if (tokenized != null && tokenized.length > 0) {
   1954                     address = tokenized[0].getAddress();
   1955                 }
   1956             }
   1957             Rfc822Token token = new Rfc822Token(display, address, null);
   1958             trimmedDisplayText = token.toString().trim();
   1959         }
   1960         int index = trimmedDisplayText.indexOf(",");
   1961         return mTokenizer != null && !TextUtils.isEmpty(trimmedDisplayText)
   1962                 && index < trimmedDisplayText.length() - 1 ? (String) mTokenizer
   1963                 .terminateToken(trimmedDisplayText) : trimmedDisplayText;
   1964     }
   1965 
   1966     // Visible for testing.
   1967     // Use this method to generate text to display in a chip.
   1968     /*package*/ String createChipDisplayText(RecipientEntry entry) {
   1969         String display = entry.getDisplayName();
   1970         String address = entry.getDestination();
   1971         if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
   1972             display = null;
   1973         }
   1974         if (!TextUtils.isEmpty(display)) {
   1975             return display;
   1976         } else if (!TextUtils.isEmpty(address)){
   1977             return address;
   1978         } else {
   1979             return new Rfc822Token(display, address, null).toString();
   1980         }
   1981     }
   1982 
   1983     private CharSequence createChip(RecipientEntry entry) {
   1984         final String displayText = createAddressText(entry);
   1985         if (TextUtils.isEmpty(displayText)) {
   1986             return null;
   1987         }
   1988         // Always leave a blank space at the end of a chip.
   1989         final int textLength = displayText.length() - 1;
   1990         final SpannableString  chipText = new SpannableString(displayText);
   1991         if (!mNoChipMode) {
   1992             try {
   1993                 DrawableRecipientChip chip = constructChipSpan(entry);
   1994                 chipText.setSpan(chip, 0, textLength,
   1995                         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   1996                 chip.setOriginalText(chipText.toString());
   1997             } catch (NullPointerException e) {
   1998                 Log.e(TAG, e.getMessage(), e);
   1999                 return null;
   2000             }
   2001         }
   2002         onChipCreated(entry);
   2003         return chipText;
   2004     }
   2005 
   2006     /**
   2007      * A callback for subclasses to use to know when a chip was created with the
   2008      * given RecipientEntry.
   2009      */
   2010     protected void onChipCreated(RecipientEntry entry) {
   2011         if (!mNoChipMode && mRecipientChipAddedListener != null) {
   2012             mRecipientChipAddedListener.onRecipientChipAdded(entry);
   2013         }
   2014     }
   2015 
   2016     /**
   2017      * When an item in the suggestions list has been clicked, create a chip from the
   2018      * contact information of the selected item.
   2019      */
   2020     @Override
   2021     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   2022         if (position < 0) {
   2023             return;
   2024         }
   2025 
   2026         final RecipientEntry entry = getAdapter().getItem(position);
   2027         if (entry.getEntryType() == RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST) {
   2028             if (mPermissionsRequestItemClickedListener != null) {
   2029                 mPermissionsRequestItemClickedListener
   2030                         .onPermissionsRequestItemClicked(this, entry.getPermissions());
   2031             }
   2032             return;
   2033         }
   2034 
   2035         final int charactersTyped = submitItemAtPosition(position);
   2036         if (charactersTyped > -1 && mRecipientEntryItemClickedListener != null) {
   2037             mRecipientEntryItemClickedListener
   2038                     .onRecipientEntryItemClicked(charactersTyped, position);
   2039         }
   2040     }
   2041 
   2042     private int submitItemAtPosition(int position) {
   2043         RecipientEntry entry = createValidatedEntry(getAdapter().getItem(position));
   2044         if (entry == null) {
   2045             return -1;
   2046         }
   2047         clearComposingText();
   2048 
   2049         int end = getSelectionEnd();
   2050         int start = mTokenizer.findTokenStart(getText(), end);
   2051 
   2052         Editable editable = getText();
   2053         QwertyKeyListener.markAsReplaced(editable, start, end, "");
   2054         CharSequence chip = createChip(entry);
   2055         if (chip != null && start >= 0 && end >= 0) {
   2056             editable.replace(start, end, chip);
   2057         }
   2058         sanitizeBetween();
   2059 
   2060         return end - start;
   2061     }
   2062 
   2063     private RecipientEntry createValidatedEntry(RecipientEntry item) {
   2064         if (item == null) {
   2065             return null;
   2066         }
   2067         final RecipientEntry entry;
   2068         // If the display name and the address are the same, or if this is a
   2069         // valid contact, but the destination is invalid, then make this a fake
   2070         // recipient that is editable.
   2071         String destination = item.getDestination();
   2072         if (!isPhoneQuery() && item.getContactId() == RecipientEntry.GENERATED_CONTACT) {
   2073             entry = RecipientEntry.constructGeneratedEntry(item.getDisplayName(),
   2074                     destination, item.isValid());
   2075         } else if (RecipientEntry.isCreatedRecipient(item.getContactId())
   2076                 && (TextUtils.isEmpty(item.getDisplayName())
   2077                         || TextUtils.equals(item.getDisplayName(), destination)
   2078                         || (mValidator != null && !mValidator.isValid(destination)))) {
   2079             entry = RecipientEntry.constructFakeEntry(destination, item.isValid());
   2080         } else {
   2081             entry = item;
   2082         }
   2083         return entry;
   2084     }
   2085 
   2086     // Visible for testing.
   2087     /* package */DrawableRecipientChip[] getSortedRecipients() {
   2088         DrawableRecipientChip[] recips = getSpannable()
   2089                 .getSpans(0, getText().length(), DrawableRecipientChip.class);
   2090         ArrayList<DrawableRecipientChip> recipientsList = new ArrayList<DrawableRecipientChip>(
   2091                 Arrays.asList(recips));
   2092         final Spannable spannable = getSpannable();
   2093         Collections.sort(recipientsList, new Comparator<DrawableRecipientChip>() {
   2094 
   2095             @Override
   2096             public int compare(DrawableRecipientChip first, DrawableRecipientChip second) {
   2097                 int firstStart = spannable.getSpanStart(first);
   2098                 int secondStart = spannable.getSpanStart(second);
   2099                 if (firstStart < secondStart) {
   2100                     return -1;
   2101                 } else if (firstStart > secondStart) {
   2102                     return 1;
   2103                 } else {
   2104                     return 0;
   2105                 }
   2106             }
   2107         });
   2108         return recipientsList.toArray(new DrawableRecipientChip[recipientsList.size()]);
   2109     }
   2110 
   2111     @Override
   2112     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
   2113         return false;
   2114     }
   2115 
   2116     @Override
   2117     public void onDestroyActionMode(ActionMode mode) {
   2118     }
   2119 
   2120     @Override
   2121     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
   2122         return false;
   2123     }
   2124 
   2125     /**
   2126      * No chips are selectable.
   2127      */
   2128     @Override
   2129     public boolean onCreateActionMode(ActionMode mode, Menu menu) {
   2130         return false;
   2131     }
   2132 
   2133     // Visible for testing.
   2134     /* package */ReplacementDrawableSpan getMoreChip() {
   2135         MoreImageSpan[] moreSpans = getSpannable().getSpans(0, getText().length(),
   2136                 MoreImageSpan.class);
   2137         return moreSpans != null && moreSpans.length > 0 ? moreSpans[0] : null;
   2138     }
   2139 
   2140     private MoreImageSpan createMoreSpan(int count) {
   2141         String moreText = String.format(mMoreItem.getText().toString(), count);
   2142         mWorkPaint.set(getPaint());
   2143         mWorkPaint.setTextSize(mMoreItem.getTextSize());
   2144         mWorkPaint.setColor(mMoreItem.getCurrentTextColor());
   2145         final int width = (int) mWorkPaint.measureText(moreText) + mMoreItem.getPaddingLeft()
   2146                 + mMoreItem.getPaddingRight();
   2147         final int height = (int) mChipHeight;
   2148         Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
   2149         Canvas canvas = new Canvas(drawable);
   2150         int adjustedHeight = height;
   2151         Layout layout = getLayout();
   2152         if (layout != null) {
   2153             adjustedHeight -= layout.getLineDescent(0);
   2154         }
   2155         canvas.drawText(moreText, 0, moreText.length(), 0, adjustedHeight, mWorkPaint);
   2156 
   2157         Drawable result = new BitmapDrawable(getResources(), drawable);
   2158         result.setBounds(0, 0, width, height);
   2159         return new MoreImageSpan(result);
   2160     }
   2161 
   2162     // Visible for testing.
   2163     /*package*/ void createMoreChipPlainText() {
   2164         // Take the first <= CHIP_LIMIT addresses and get to the end of the second one.
   2165         Editable text = getText();
   2166         int start = 0;
   2167         int end = start;
   2168         for (int i = 0; i < CHIP_LIMIT; i++) {
   2169             end = movePastTerminators(mTokenizer.findTokenEnd(text, start));
   2170             start = end; // move to the next token and get its end.
   2171         }
   2172         // Now, count total addresses.
   2173         int tokenCount = countTokens(text);
   2174         MoreImageSpan moreSpan = createMoreSpan(tokenCount - CHIP_LIMIT);
   2175         SpannableString chipText = new SpannableString(text.subSequence(end, text.length()));
   2176         chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2177         text.replace(end, text.length(), chipText);
   2178         mMoreChip = moreSpan;
   2179     }
   2180 
   2181     // Visible for testing.
   2182     /* package */int countTokens(Editable text) {
   2183         int tokenCount = 0;
   2184         int start = 0;
   2185         while (start < text.length()) {
   2186             start = movePastTerminators(mTokenizer.findTokenEnd(text, start));
   2187             tokenCount++;
   2188             if (start >= text.length()) {
   2189                 break;
   2190             }
   2191         }
   2192         return tokenCount;
   2193     }
   2194 
   2195     /**
   2196      * Create the more chip. The more chip is text that replaces any chips that
   2197      * do not fit in the pre-defined available space when the
   2198      * RecipientEditTextView loses focus.
   2199      */
   2200     // Visible for testing.
   2201     /* package */ void createMoreChip() {
   2202         if (mNoChipMode) {
   2203             createMoreChipPlainText();
   2204             return;
   2205         }
   2206 
   2207         if (!mShouldShrink) {
   2208             return;
   2209         }
   2210         ReplacementDrawableSpan[] tempMore = getSpannable().getSpans(0, getText().length(),
   2211                 MoreImageSpan.class);
   2212         if (tempMore.length > 0) {
   2213             getSpannable().removeSpan(tempMore[0]);
   2214         }
   2215         DrawableRecipientChip[] recipients = getSortedRecipients();
   2216 
   2217         if (recipients == null || recipients.length <= CHIP_LIMIT) {
   2218             mMoreChip = null;
   2219             return;
   2220         }
   2221         Spannable spannable = getSpannable();
   2222         int numRecipients = recipients.length;
   2223         int overage = numRecipients - CHIP_LIMIT;
   2224         MoreImageSpan moreSpan = createMoreSpan(overage);
   2225         mHiddenSpans = new ArrayList<DrawableRecipientChip>();
   2226         int totalReplaceStart = 0;
   2227         int totalReplaceEnd = 0;
   2228         Editable text = getText();
   2229         for (int i = numRecipients - overage; i < recipients.length; i++) {
   2230             mHiddenSpans.add(recipients[i]);
   2231             if (i == numRecipients - overage) {
   2232                 totalReplaceStart = spannable.getSpanStart(recipients[i]);
   2233             }
   2234             if (i == recipients.length - 1) {
   2235                 totalReplaceEnd = spannable.getSpanEnd(recipients[i]);
   2236             }
   2237             if (mTemporaryRecipients == null || !mTemporaryRecipients.contains(recipients[i])) {
   2238                 int spanStart = spannable.getSpanStart(recipients[i]);
   2239                 int spanEnd = spannable.getSpanEnd(recipients[i]);
   2240                 recipients[i].setOriginalText(text.toString().substring(spanStart, spanEnd));
   2241             }
   2242             spannable.removeSpan(recipients[i]);
   2243         }
   2244         if (totalReplaceEnd < text.length()) {
   2245             totalReplaceEnd = text.length();
   2246         }
   2247         int end = Math.max(totalReplaceStart, totalReplaceEnd);
   2248         int start = Math.min(totalReplaceStart, totalReplaceEnd);
   2249         SpannableString chipText = new SpannableString(text.subSequence(start, end));
   2250         chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2251         text.replace(start, end, chipText);
   2252         mMoreChip = moreSpan;
   2253         // If adding the +more chip goes over the limit, resize accordingly.
   2254         if (!isPhoneQuery() && getLineCount() > mMaxLines) {
   2255             setMaxLines(getLineCount());
   2256         }
   2257     }
   2258 
   2259     /**
   2260      * Replace the more chip, if it exists, with all of the recipient chips it had
   2261      * replaced when the RecipientEditTextView gains focus.
   2262      */
   2263     // Visible for testing.
   2264     /*package*/ void removeMoreChip() {
   2265         if (mMoreChip != null) {
   2266             Spannable span = getSpannable();
   2267             span.removeSpan(mMoreChip);
   2268             mMoreChip = null;
   2269             // Re-add the spans that were hidden.
   2270             if (mHiddenSpans != null && mHiddenSpans.size() > 0) {
   2271                 // Recreate each hidden span.
   2272                 DrawableRecipientChip[] recipients = getSortedRecipients();
   2273                 // Start the search for tokens after the last currently visible
   2274                 // chip.
   2275                 if (recipients == null || recipients.length == 0) {
   2276                     return;
   2277                 }
   2278                 int end = span.getSpanEnd(recipients[recipients.length - 1]);
   2279                 Editable editable = getText();
   2280                 for (DrawableRecipientChip chip : mHiddenSpans) {
   2281                     int chipStart;
   2282                     int chipEnd;
   2283                     String token;
   2284                     // Need to find the location of the chip, again.
   2285                     token = (String) chip.getOriginalText();
   2286                     // As we find the matching recipient for the hidden spans,
   2287                     // reduce the size of the string we need to search.
   2288                     // That way, if there are duplicates, we always find the correct
   2289                     // recipient.
   2290                     chipStart = editable.toString().indexOf(token, end);
   2291                     end = chipEnd = Math.min(editable.length(), chipStart + token.length());
   2292                     // Only set the span if we found a matching token.
   2293                     if (chipStart != -1) {
   2294                         editable.setSpan(chip, chipStart, chipEnd,
   2295                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   2296                     }
   2297                 }
   2298                 mHiddenSpans.clear();
   2299             }
   2300         }
   2301     }
   2302 
   2303     /**
   2304      * Show specified chip as selected. If the RecipientChip is just an email address,
   2305      * selecting the chip will take the contents of the chip and place it at
   2306      * the end of the RecipientEditTextView for inline editing. If the
   2307      * RecipientChip is a complete contact, then selecting the chip
   2308      * will show a popup window with the address in use highlighted and any other
   2309      * alternate addresses for the contact.
   2310      * @param currentChip Chip to select.
   2311      */
   2312     private void selectChip(DrawableRecipientChip currentChip) {
   2313         if (shouldShowEditableText(currentChip)) {
   2314             CharSequence text = currentChip.getValue();
   2315             Editable editable = getText();
   2316             Spannable spannable = getSpannable();
   2317             int spanStart = spannable.getSpanStart(currentChip);
   2318             int spanEnd = spannable.getSpanEnd(currentChip);
   2319             spannable.removeSpan(currentChip);
   2320             // Don't need leading space if it's the only chip
   2321             if (spanEnd - spanStart == editable.length() - 1) {
   2322                 spanEnd++;
   2323             }
   2324             editable.delete(spanStart, spanEnd);
   2325             setCursorVisible(true);
   2326             setSelection(editable.length());
   2327             editable.append(text);
   2328             mSelectedChip = constructChipSpan(
   2329                     RecipientEntry.constructFakeEntry((String) text, isValid(text.toString())));
   2330 
   2331             /*
   2332              * Because chip is destroyed and converted into an editable text, we call
   2333              * {@link RecipientChipDeletedListener#onRecipientChipDeleted}. For the cases where
   2334              * editable text is not shown (i.e. chip is in user's contact list), chip is focused
   2335              * and below callback is not called.
   2336              */
   2337             if (!mNoChipMode && mRecipientChipDeletedListener != null) {
   2338                 mRecipientChipDeletedListener.onRecipientChipDeleted(currentChip.getEntry());
   2339             }
   2340         } else {
   2341             final boolean showAddress =
   2342                     currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT ||
   2343                     getAdapter().forceShowAddress();
   2344             if (showAddress && mNoChipMode) {
   2345                 return;
   2346             }
   2347 
   2348             if (isTouchExplorationEnabled()) {
   2349                 // The chips cannot be touch-explored. However, doing a double-tap results in
   2350                 // the popup being shown for the last chip, which is of no value.
   2351                 return;
   2352             }
   2353 
   2354             mSelectedChip = currentChip;
   2355             setSelection(getText().getSpanEnd(mSelectedChip));
   2356             setCursorVisible(false);
   2357 
   2358             if (showAddress) {
   2359                 showAddress(currentChip, mAddressPopup);
   2360             } else {
   2361                 showAlternates(currentChip, mAlternatesPopup);
   2362             }
   2363         }
   2364     }
   2365 
   2366     private boolean isTouchExplorationEnabled() {
   2367         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
   2368             return false;
   2369         }
   2370 
   2371         final AccessibilityManager accessibilityManager = (AccessibilityManager)
   2372                 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
   2373         return accessibilityManager.isTouchExplorationEnabled();
   2374     }
   2375 
   2376     private boolean shouldShowEditableText(DrawableRecipientChip currentChip) {
   2377         long contactId = currentChip.getContactId();
   2378         return contactId == RecipientEntry.INVALID_CONTACT
   2379                 || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
   2380     }
   2381 
   2382     private void showAddress(final DrawableRecipientChip currentChip, final ListPopupWindow popup) {
   2383         if (!mAttachedToWindow) {
   2384             return;
   2385         }
   2386         int line = getLayout().getLineForOffset(getChipStart(currentChip));
   2387         int bottomOffset = calculateOffsetFromBottomToTop(line);
   2388         // Align the alternates popup with the left side of the View,
   2389         // regardless of the position of the chip tapped.
   2390         popup.setAnchorView((mAlternatePopupAnchor != null) ? mAlternatePopupAnchor : this);
   2391         popup.setVerticalOffset(bottomOffset);
   2392         popup.setAdapter(createSingleAddressAdapter(currentChip));
   2393         popup.setOnItemClickListener(new OnItemClickListener() {
   2394             @Override
   2395             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   2396                 unselectChip(currentChip);
   2397                 popup.dismiss();
   2398             }
   2399         });
   2400         popup.show();
   2401         ListView listView = popup.getListView();
   2402         listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
   2403         listView.setItemChecked(0, true);
   2404     }
   2405 
   2406     /**
   2407      * Remove selection from this chip. Unselecting a RecipientChip will render
   2408      * the chip without a delete icon and with an unfocused background. This is
   2409      * called when the RecipientChip no longer has focus.
   2410      */
   2411     private void unselectChip(DrawableRecipientChip chip) {
   2412         int start = getChipStart(chip);
   2413         int end = getChipEnd(chip);
   2414         Editable editable = getText();
   2415         mSelectedChip = null;
   2416         if (start == -1 || end == -1) {
   2417             Log.w(TAG, "The chip doesn't exist or may be a chip a user was editing");
   2418             setSelection(editable.length());
   2419             commitDefault();
   2420         } else {
   2421             getSpannable().removeSpan(chip);
   2422             QwertyKeyListener.markAsReplaced(editable, start, end, "");
   2423             editable.removeSpan(chip);
   2424             try {
   2425                 if (!mNoChipMode) {
   2426                     editable.setSpan(constructChipSpan(chip.getEntry()),
   2427                             start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2428                 }
   2429             } catch (NullPointerException e) {
   2430                 Log.e(TAG, e.getMessage(), e);
   2431             }
   2432         }
   2433         setCursorVisible(true);
   2434         setSelection(editable.length());
   2435         if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
   2436             mAlternatesPopup.dismiss();
   2437         }
   2438     }
   2439 
   2440     @Override
   2441     public void onChipDelete() {
   2442         if (mSelectedChip != null) {
   2443             if (!mNoChipMode && mRecipientChipDeletedListener != null) {
   2444                 mRecipientChipDeletedListener.onRecipientChipDeleted(mSelectedChip.getEntry());
   2445             }
   2446             removeChip(mSelectedChip);
   2447         }
   2448         dismissPopups();
   2449     }
   2450 
   2451     @Override
   2452     public void onPermissionRequestDismissed() {
   2453         if (mPermissionsRequestItemClickedListener != null) {
   2454             mPermissionsRequestItemClickedListener.onPermissionRequestDismissed();
   2455         }
   2456         dismissDropDown();
   2457     }
   2458 
   2459     private void dismissPopups() {
   2460         if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
   2461             mAlternatesPopup.dismiss();
   2462         }
   2463         if (mAddressPopup != null && mAddressPopup.isShowing()) {
   2464             mAddressPopup.dismiss();
   2465         }
   2466         setSelection(getText().length());
   2467     }
   2468 
   2469     /**
   2470      * Remove the chip and any text associated with it from the RecipientEditTextView.
   2471      */
   2472     // Visible for testing.
   2473     /* package */void removeChip(DrawableRecipientChip chip) {
   2474         Spannable spannable = getSpannable();
   2475         int spanStart = spannable.getSpanStart(chip);
   2476         int spanEnd = spannable.getSpanEnd(chip);
   2477         Editable text = getText();
   2478         int toDelete = spanEnd;
   2479         boolean wasSelected = chip == mSelectedChip;
   2480         // Clear that there is a selected chip before updating any text.
   2481         if (wasSelected) {
   2482             mSelectedChip = null;
   2483         }
   2484         // Always remove trailing spaces when removing a chip.
   2485         while (toDelete >= 0 && toDelete < text.length() && text.charAt(toDelete) == ' ') {
   2486             toDelete++;
   2487         }
   2488         spannable.removeSpan(chip);
   2489         if (spanStart >= 0 && toDelete > 0) {
   2490             text.delete(spanStart, toDelete);
   2491         }
   2492         if (wasSelected) {
   2493             clearSelectedChip();
   2494         }
   2495     }
   2496 
   2497     /**
   2498      * Replace this currently selected chip with a new chip
   2499      * that uses the contact data provided.
   2500      */
   2501     // Visible for testing.
   2502     /*package*/ void replaceChip(DrawableRecipientChip chip, RecipientEntry entry) {
   2503         boolean wasSelected = chip == mSelectedChip;
   2504         if (wasSelected) {
   2505             mSelectedChip = null;
   2506         }
   2507         int start = getChipStart(chip);
   2508         int end = getChipEnd(chip);
   2509         getSpannable().removeSpan(chip);
   2510         Editable editable = getText();
   2511         CharSequence chipText = createChip(entry);
   2512         if (chipText != null) {
   2513             if (start == -1 || end == -1) {
   2514                 Log.e(TAG, "The chip to replace does not exist but should.");
   2515                 editable.insert(0, chipText);
   2516             } else {
   2517                 if (!TextUtils.isEmpty(chipText)) {
   2518                     // There may be a space to replace with this chip's new
   2519                     // associated space. Check for it
   2520                     int toReplace = end;
   2521                     while (toReplace >= 0 && toReplace < editable.length()
   2522                             && editable.charAt(toReplace) == ' ') {
   2523                         toReplace++;
   2524                     }
   2525                     editable.replace(start, toReplace, chipText);
   2526                 }
   2527             }
   2528         }
   2529         setCursorVisible(true);
   2530         if (wasSelected) {
   2531             clearSelectedChip();
   2532         }
   2533     }
   2534 
   2535     /**
   2536      * Handle click events for a chip. When a selected chip receives a click
   2537      * event, see if that event was in the delete icon. If so, delete it.
   2538      * Otherwise, unselect the chip.
   2539      */
   2540     public void onClick(DrawableRecipientChip chip) {
   2541         if (chip.isSelected()) {
   2542             clearSelectedChip();
   2543         }
   2544     }
   2545 
   2546     private boolean chipsPending() {
   2547         return mPendingChipsCount > 0 || (mHiddenSpans != null && mHiddenSpans.size() > 0);
   2548     }
   2549 
   2550     @Override
   2551     public void removeTextChangedListener(TextWatcher watcher) {
   2552         mTextWatcher = null;
   2553         super.removeTextChangedListener(watcher);
   2554     }
   2555 
   2556     private boolean isValidEmailAddress(String input) {
   2557         return !TextUtils.isEmpty(input) && mValidator != null &&
   2558                 mValidator.isValid(input);
   2559     }
   2560 
   2561     private class RecipientTextWatcher implements TextWatcher {
   2562 
   2563         @Override
   2564         public void afterTextChanged(Editable s) {
   2565             // If the text has been set to null or empty, make sure we remove
   2566             // all the spans we applied.
   2567             if (TextUtils.isEmpty(s)) {
   2568                 // Remove all the chips spans.
   2569                 Spannable spannable = getSpannable();
   2570                 DrawableRecipientChip[] chips = spannable.getSpans(0, getText().length(),
   2571                         DrawableRecipientChip.class);
   2572                 for (DrawableRecipientChip chip : chips) {
   2573                     spannable.removeSpan(chip);
   2574                 }
   2575                 if (mMoreChip != null) {
   2576                     spannable.removeSpan(mMoreChip);
   2577                 }
   2578                 clearSelectedChip();
   2579                 return;
   2580             }
   2581             // Get whether there are any recipients pending addition to the
   2582             // view. If there are, don't do anything in the text watcher.
   2583             if (chipsPending()) {
   2584                 return;
   2585             }
   2586             // If the user is editing a chip, don't clear it.
   2587             if (mSelectedChip != null) {
   2588                 if (!isGeneratedContact(mSelectedChip)) {
   2589                     setCursorVisible(true);
   2590                     setSelection(getText().length());
   2591                     clearSelectedChip();
   2592                 } else {
   2593                     return;
   2594                 }
   2595             }
   2596             int length = s.length();
   2597             // Make sure there is content there to parse and that it is
   2598             // not just the commit character.
   2599             if (length > 1) {
   2600                 if (lastCharacterIsCommitCharacter(s)) {
   2601                     commitByCharacter();
   2602                     return;
   2603                 }
   2604                 char last;
   2605                 int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
   2606                 int len = length() - 1;
   2607                 if (end != len) {
   2608                     last = s.charAt(end);
   2609                 } else {
   2610                     last = s.charAt(len);
   2611                 }
   2612                 if (last == COMMIT_CHAR_SPACE) {
   2613                     if (!isPhoneQuery()) {
   2614                         // Check if this is a valid email address. If it is,
   2615                         // commit it.
   2616                         String text = getText().toString();
   2617                         int tokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
   2618                         String sub = text.substring(tokenStart, mTokenizer.findTokenEnd(text,
   2619                                 tokenStart));
   2620                         if (isValidEmailAddress(sub)) {
   2621                             commitByCharacter();
   2622                         }
   2623                     }
   2624                 }
   2625             }
   2626         }
   2627 
   2628         @Override
   2629         public void onTextChanged(CharSequence s, int start, int before, int count) {
   2630             // The user deleted some text OR some text was replaced; check to
   2631             // see if the insertion point is on a space
   2632             // following a chip.
   2633             if (before - count == 1) {
   2634                 // If the item deleted is a space, and the thing before the
   2635                 // space is a chip, delete the entire span.
   2636                 int selStart = getSelectionStart();
   2637                 DrawableRecipientChip[] repl = getSpannable().getSpans(selStart, selStart,
   2638                         DrawableRecipientChip.class);
   2639                 if (repl.length > 0) {
   2640                     // There is a chip there! Just remove it.
   2641                     DrawableRecipientChip toDelete = repl[0];
   2642                     Editable editable = getText();
   2643                     // Add the separator token.
   2644                     int deleteStart = editable.getSpanStart(toDelete);
   2645                     int deleteEnd = editable.getSpanEnd(toDelete) + 1;
   2646                     if (deleteEnd > editable.length()) {
   2647                         deleteEnd = editable.length();
   2648                     }
   2649                     if (!mNoChipMode && mRecipientChipDeletedListener != null) {
   2650                         mRecipientChipDeletedListener.onRecipientChipDeleted(toDelete.getEntry());
   2651                     }
   2652                     editable.removeSpan(toDelete);
   2653                     editable.delete(deleteStart, deleteEnd);
   2654                 }
   2655             } else if (count > before) {
   2656                 if (mSelectedChip != null
   2657                     && isGeneratedContact(mSelectedChip)) {
   2658                     if (lastCharacterIsCommitCharacter(s)) {
   2659                         commitByCharacter();
   2660                         return;
   2661                     }
   2662                 }
   2663             }
   2664         }
   2665 
   2666         @Override
   2667         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
   2668             // Do nothing.
   2669         }
   2670     }
   2671 
   2672     public boolean lastCharacterIsCommitCharacter(CharSequence s) {
   2673         char last;
   2674         int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
   2675         int len = length() - 1;
   2676         if (end != len) {
   2677             last = s.charAt(end);
   2678         } else {
   2679             last = s.charAt(len);
   2680         }
   2681         return last == COMMIT_CHAR_COMMA || last == COMMIT_CHAR_SEMICOLON;
   2682     }
   2683 
   2684     public boolean isGeneratedContact(DrawableRecipientChip chip) {
   2685         long contactId = chip.getContactId();
   2686         return contactId == RecipientEntry.INVALID_CONTACT
   2687                 || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
   2688     }
   2689 
   2690     /**
   2691      * Handles pasting a {@link ClipData} to this {@link RecipientEditTextView}.
   2692      */
   2693     // Visible for testing.
   2694     void handlePasteClip(ClipData clip) {
   2695         if (clip == null) {
   2696             // Do nothing.
   2697             return;
   2698         }
   2699 
   2700         final ClipDescription clipDesc = clip.getDescription();
   2701         boolean containsSupportedType = clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
   2702                 clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
   2703         if (!containsSupportedType) {
   2704             return;
   2705         }
   2706 
   2707         removeTextChangedListener(mTextWatcher);
   2708 
   2709         final ClipDescription clipDescription = clip.getDescription();
   2710         for (int i = 0; i < clip.getItemCount(); i++) {
   2711             final String mimeType = clipDescription.getMimeType(i);
   2712             final boolean supportedType = ClipDescription.MIMETYPE_TEXT_PLAIN.equals(mimeType) ||
   2713                     ClipDescription.MIMETYPE_TEXT_HTML.equals(mimeType);
   2714             if (!supportedType) {
   2715                 // Only plain text and html can be pasted.
   2716                 continue;
   2717             }
   2718 
   2719             final CharSequence pastedItem = clip.getItemAt(i).getText();
   2720             if (!TextUtils.isEmpty(pastedItem)) {
   2721                 final Editable editable = getText();
   2722                 final int start = getSelectionStart();
   2723                 final int end = getSelectionEnd();
   2724                 if (start < 0 || end < 1) {
   2725                     // No selection.
   2726                     editable.append(pastedItem);
   2727                 } else if (start == end) {
   2728                     // Insert at position.
   2729                     editable.insert(start, pastedItem);
   2730                 } else {
   2731                     editable.append(pastedItem, start, end);
   2732                 }
   2733                 handlePasteAndReplace();
   2734             }
   2735         }
   2736 
   2737         mHandler.post(mAddTextWatcher);
   2738     }
   2739 
   2740     @Override
   2741     public boolean onTextContextMenuItem(int id) {
   2742         if (id == android.R.id.paste) {
   2743             ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
   2744                     Context.CLIPBOARD_SERVICE);
   2745             handlePasteClip(clipboard.getPrimaryClip());
   2746             return true;
   2747         }
   2748         return super.onTextContextMenuItem(id);
   2749     }
   2750 
   2751     private void handlePasteAndReplace() {
   2752         ArrayList<DrawableRecipientChip> created = handlePaste();
   2753         if (created != null && created.size() > 0) {
   2754             // Perform reverse lookups on the pasted contacts.
   2755             IndividualReplacementTask replace = new IndividualReplacementTask();
   2756             replace.execute(created);
   2757         }
   2758     }
   2759 
   2760     // Visible for testing.
   2761     /* package */ArrayList<DrawableRecipientChip> handlePaste() {
   2762         String text = getText().toString();
   2763         int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
   2764         String lastAddress = text.substring(originalTokenStart);
   2765         int tokenStart = originalTokenStart;
   2766         int prevTokenStart = 0;
   2767         DrawableRecipientChip findChip = null;
   2768         ArrayList<DrawableRecipientChip> created = new ArrayList<DrawableRecipientChip>();
   2769         if (tokenStart != 0) {
   2770             // There are things before this!
   2771             while (tokenStart != 0 && findChip == null && tokenStart != prevTokenStart) {
   2772                 prevTokenStart = tokenStart;
   2773                 tokenStart = mTokenizer.findTokenStart(text, tokenStart);
   2774                 findChip = findChip(tokenStart);
   2775                 if (tokenStart == originalTokenStart && findChip == null) {
   2776                     break;
   2777                 }
   2778             }
   2779             if (tokenStart != originalTokenStart) {
   2780                 if (findChip != null) {
   2781                     tokenStart = prevTokenStart;
   2782                 }
   2783                 int tokenEnd;
   2784                 DrawableRecipientChip createdChip;
   2785                 while (tokenStart < originalTokenStart) {
   2786                     tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(getText().toString(),
   2787                             tokenStart));
   2788                     commitChip(tokenStart, tokenEnd, getText());
   2789                     createdChip = findChip(tokenStart);
   2790                     if (createdChip == null) {
   2791                         break;
   2792                     }
   2793                     // +1 for the space at the end.
   2794                     tokenStart = getSpannable().getSpanEnd(createdChip) + 1;
   2795                     created.add(createdChip);
   2796                 }
   2797             }
   2798         }
   2799         // Take a look at the last token. If the token has been completed with a
   2800         // commit character, create a chip.
   2801         if (isCompletedToken(lastAddress)) {
   2802             Editable editable = getText();
   2803             tokenStart = editable.toString().indexOf(lastAddress, originalTokenStart);
   2804             commitChip(tokenStart, editable.length(), editable);
   2805             created.add(findChip(tokenStart));
   2806         }
   2807         return created;
   2808     }
   2809 
   2810     // Visible for testing.
   2811     /* package */int movePastTerminators(int tokenEnd) {
   2812         if (tokenEnd >= length()) {
   2813             return tokenEnd;
   2814         }
   2815         char atEnd = getText().toString().charAt(tokenEnd);
   2816         if (atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON) {
   2817             tokenEnd++;
   2818         }
   2819         // This token had not only an end token character, but also a space
   2820         // separating it from the next token.
   2821         if (tokenEnd < length() && getText().toString().charAt(tokenEnd) == ' ') {
   2822             tokenEnd++;
   2823         }
   2824         return tokenEnd;
   2825     }
   2826 
   2827     private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> {
   2828         private DrawableRecipientChip createFreeChip(RecipientEntry entry) {
   2829             try {
   2830                 if (mNoChipMode) {
   2831                     return null;
   2832                 }
   2833                 return constructChipSpan(entry);
   2834             } catch (NullPointerException e) {
   2835                 Log.e(TAG, e.getMessage(), e);
   2836                 return null;
   2837             }
   2838         }
   2839 
   2840         @Override
   2841         protected void onPreExecute() {
   2842             // Ensure everything is in chip-form already, so we don't have text that slowly gets
   2843             // replaced
   2844             final List<DrawableRecipientChip> originalRecipients =
   2845                     new ArrayList<DrawableRecipientChip>();
   2846             final DrawableRecipientChip[] existingChips = getSortedRecipients();
   2847             Collections.addAll(originalRecipients, existingChips);
   2848             if (mHiddenSpans != null) {
   2849                 originalRecipients.addAll(mHiddenSpans);
   2850             }
   2851 
   2852             final List<DrawableRecipientChip> replacements =
   2853                     new ArrayList<DrawableRecipientChip>(originalRecipients.size());
   2854 
   2855             for (final DrawableRecipientChip chip : originalRecipients) {
   2856                 if (RecipientEntry.isCreatedRecipient(chip.getEntry().getContactId())
   2857                         && getSpannable().getSpanStart(chip) != -1) {
   2858                     replacements.add(createFreeChip(chip.getEntry()));
   2859                 } else {
   2860                     replacements.add(null);
   2861                 }
   2862             }
   2863 
   2864             processReplacements(originalRecipients, replacements);
   2865         }
   2866 
   2867         @Override
   2868         protected Void doInBackground(Void... params) {
   2869             if (mIndividualReplacements != null) {
   2870                 mIndividualReplacements.cancel(true);
   2871             }
   2872             // For each chip in the list, look up the matching contact.
   2873             // If there is a match, replace that chip with the matching
   2874             // chip.
   2875             final ArrayList<DrawableRecipientChip> recipients =
   2876                     new ArrayList<DrawableRecipientChip>();
   2877             DrawableRecipientChip[] existingChips = getSortedRecipients();
   2878             Collections.addAll(recipients, existingChips);
   2879             if (mHiddenSpans != null) {
   2880                 recipients.addAll(mHiddenSpans);
   2881             }
   2882             ArrayList<String> addresses = new ArrayList<String>();
   2883             for (DrawableRecipientChip chip : recipients) {
   2884                 if (chip != null) {
   2885                     addresses.add(createAddressText(chip.getEntry()));
   2886                 }
   2887             }
   2888             final BaseRecipientAdapter adapter = getAdapter();
   2889             adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
   2890                         @Override
   2891                         public void matchesFound(Map<String, RecipientEntry> entries) {
   2892                             final ArrayList<DrawableRecipientChip> replacements =
   2893                                     new ArrayList<DrawableRecipientChip>();
   2894                             for (final DrawableRecipientChip temp : recipients) {
   2895                                 RecipientEntry entry = null;
   2896                                 if (temp != null && RecipientEntry.isCreatedRecipient(
   2897                                         temp.getEntry().getContactId())
   2898                                         && getSpannable().getSpanStart(temp) != -1) {
   2899                                     // Replace this.
   2900                                     entry = createValidatedEntry(
   2901                                             entries.get(tokenizeAddress(temp.getEntry()
   2902                                                     .getDestination())));
   2903                                 }
   2904                                 if (entry != null) {
   2905                                     replacements.add(createFreeChip(entry));
   2906                                 } else {
   2907                                     replacements.add(null);
   2908                                 }
   2909                             }
   2910                             processReplacements(recipients, replacements);
   2911                         }
   2912 
   2913                         @Override
   2914                         public void matchesNotFound(final Set<String> unfoundAddresses) {
   2915                             final List<DrawableRecipientChip> replacements =
   2916                                     new ArrayList<DrawableRecipientChip>(unfoundAddresses.size());
   2917 
   2918                             for (final DrawableRecipientChip temp : recipients) {
   2919                                 if (temp != null && RecipientEntry.isCreatedRecipient(
   2920                                         temp.getEntry().getContactId())
   2921                                         && getSpannable().getSpanStart(temp) != -1) {
   2922                                     if (unfoundAddresses.contains(
   2923                                             temp.getEntry().getDestination())) {
   2924                                         replacements.add(createFreeChip(temp.getEntry()));
   2925                                     } else {
   2926                                         replacements.add(null);
   2927                                     }
   2928                                 } else {
   2929                                     replacements.add(null);
   2930                                 }
   2931                             }
   2932 
   2933                             processReplacements(recipients, replacements);
   2934                         }
   2935                     });
   2936             return null;
   2937         }
   2938 
   2939         private void processReplacements(final List<DrawableRecipientChip> recipients,
   2940                 final List<DrawableRecipientChip> replacements) {
   2941             if (replacements != null && replacements.size() > 0) {
   2942                 final Runnable runnable = new Runnable() {
   2943                     @Override
   2944                     public void run() {
   2945                         final Editable text = new SpannableStringBuilder(getText());
   2946                         int i = 0;
   2947                         for (final DrawableRecipientChip chip : recipients) {
   2948                             final DrawableRecipientChip replacement = replacements.get(i);
   2949                             if (replacement != null) {
   2950                                 final RecipientEntry oldEntry = chip.getEntry();
   2951                                 final RecipientEntry newEntry = replacement.getEntry();
   2952                                 final boolean isBetter =
   2953                                         RecipientAlternatesAdapter.getBetterRecipient(
   2954                                                 oldEntry, newEntry) == newEntry;
   2955 
   2956                                 if (isBetter) {
   2957                                     // Find the location of the chip in the text currently shown.
   2958                                     final int start = text.getSpanStart(chip);
   2959                                     if (start != -1) {
   2960                                         // Replacing the entirety of what the chip represented,
   2961                                         // including the extra space dividing it from other chips.
   2962                                         final int end =
   2963                                                 Math.min(text.getSpanEnd(chip) + 1, text.length());
   2964                                         text.removeSpan(chip);
   2965                                         // Make sure we always have just 1 space at the end to
   2966                                         // separate this chip from the next chip.
   2967                                         final SpannableString displayText =
   2968                                                 new SpannableString(createAddressText(
   2969                                                         replacement.getEntry()).trim() + " ");
   2970                                         displayText.setSpan(replacement, 0,
   2971                                                 displayText.length() - 1,
   2972                                                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2973                                         // Replace the old text we found with with the new display
   2974                                         // text, which now may also contain the display name of the
   2975                                         // recipient.
   2976                                         text.replace(start, end, displayText);
   2977                                         replacement.setOriginalText(displayText.toString());
   2978                                         replacements.set(i, null);
   2979 
   2980                                         recipients.set(i, replacement);
   2981                                     }
   2982                                 }
   2983                             }
   2984                             i++;
   2985                         }
   2986                         setText(text);
   2987                     }
   2988                 };
   2989 
   2990                 if (Looper.myLooper() == Looper.getMainLooper()) {
   2991                     runnable.run();
   2992                 } else {
   2993                     mHandler.post(runnable);
   2994                 }
   2995             }
   2996         }
   2997     }
   2998 
   2999     private class IndividualReplacementTask
   3000             extends AsyncTask<ArrayList<DrawableRecipientChip>, Void, Void> {
   3001         @Override
   3002         protected Void doInBackground(ArrayList<DrawableRecipientChip>... params) {
   3003             // For each chip in the list, look up the matching contact.
   3004             // If there is a match, replace that chip with the matching
   3005             // chip.
   3006             final ArrayList<DrawableRecipientChip> originalRecipients = params[0];
   3007             ArrayList<String> addresses = new ArrayList<String>();
   3008             for (DrawableRecipientChip chip : originalRecipients) {
   3009                 if (chip != null) {
   3010                     addresses.add(createAddressText(chip.getEntry()));
   3011                 }
   3012             }
   3013             final BaseRecipientAdapter adapter = getAdapter();
   3014             adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
   3015 
   3016                         @Override
   3017                         public void matchesFound(Map<String, RecipientEntry> entries) {
   3018                             for (final DrawableRecipientChip temp : originalRecipients) {
   3019                                 if (RecipientEntry.isCreatedRecipient(temp.getEntry()
   3020                                         .getContactId())
   3021                                         && getSpannable().getSpanStart(temp) != -1) {
   3022                                     // Replace this.
   3023                                     final RecipientEntry entry = createValidatedEntry(entries
   3024                                             .get(tokenizeAddress(temp.getEntry().getDestination())
   3025                                                     .toLowerCase()));
   3026                                     if (entry != null) {
   3027                                         mHandler.post(new Runnable() {
   3028                                             @Override
   3029                                             public void run() {
   3030                                                 replaceChip(temp, entry);
   3031                                             }
   3032                                         });
   3033                                     }
   3034                                 }
   3035                             }
   3036                         }
   3037 
   3038                         @Override
   3039                         public void matchesNotFound(final Set<String> unfoundAddresses) {
   3040                             // No action required
   3041                         }
   3042                     });
   3043             return null;
   3044         }
   3045     }
   3046 
   3047 
   3048     /**
   3049      * MoreImageSpan is a simple class created for tracking the existence of a
   3050      * more chip across activity restarts/
   3051      */
   3052     private class MoreImageSpan extends ReplacementDrawableSpan {
   3053         public MoreImageSpan(Drawable b) {
   3054             super(b);
   3055             setExtraMargin(mLineSpacingExtra);
   3056         }
   3057     }
   3058 
   3059     @Override
   3060     public boolean onDown(MotionEvent e) {
   3061         return false;
   3062     }
   3063 
   3064     @Override
   3065     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   3066         // Do nothing.
   3067         return false;
   3068     }
   3069 
   3070     @Override
   3071     public void onLongPress(MotionEvent event) {
   3072         if (mSelectedChip != null) {
   3073             return;
   3074         }
   3075         float x = event.getX();
   3076         float y = event.getY();
   3077         final int offset = putOffsetInRange(x, y);
   3078         DrawableRecipientChip currentChip = findChip(offset);
   3079         if (currentChip != null) {
   3080             if (mDragEnabled) {
   3081                 // Start drag-and-drop for the selected chip.
   3082                 startDrag(currentChip);
   3083             } else {
   3084                 // Copy the selected chip email address.
   3085                 showCopyDialog(currentChip.getEntry().getDestination());
   3086             }
   3087         }
   3088     }
   3089 
   3090     // The following methods are used to provide some functionality on older versions of Android
   3091     // These methods were copied out of JB MR2's TextView
   3092     /////////////////////////////////////////////////
   3093     private int supportGetOffsetForPosition(float x, float y) {
   3094         if (getLayout() == null) return -1;
   3095         final int line = supportGetLineAtCoordinate(y);
   3096         return supportGetOffsetAtCoordinate(line, x);
   3097     }
   3098 
   3099     private float supportConvertToLocalHorizontalCoordinate(float x) {
   3100         x -= getTotalPaddingLeft();
   3101         // Clamp the position to inside of the view.
   3102         x = Math.max(0.0f, x);
   3103         x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
   3104         x += getScrollX();
   3105         return x;
   3106     }
   3107 
   3108     private int supportGetLineAtCoordinate(float y) {
   3109         y -= getTotalPaddingLeft();
   3110         // Clamp the position to inside of the view.
   3111         y = Math.max(0.0f, y);
   3112         y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
   3113         y += getScrollY();
   3114         return getLayout().getLineForVertical((int) y);
   3115     }
   3116 
   3117     private int supportGetOffsetAtCoordinate(int line, float x) {
   3118         x = supportConvertToLocalHorizontalCoordinate(x);
   3119         return getLayout().getOffsetForHorizontal(line, x);
   3120     }
   3121     /////////////////////////////////////////////////
   3122 
   3123     /**
   3124      * Enables drag-and-drop for chips.
   3125      */
   3126     public void enableDrag() {
   3127         mDragEnabled = true;
   3128     }
   3129 
   3130     /**
   3131      * Starts drag-and-drop for the selected chip.
   3132      */
   3133     private void startDrag(DrawableRecipientChip currentChip) {
   3134         String address = currentChip.getEntry().getDestination();
   3135         ClipData data = ClipData.newPlainText(address, address + COMMIT_CHAR_COMMA);
   3136 
   3137         // Start drag mode.
   3138         startDrag(data, new RecipientChipShadow(currentChip), null, 0);
   3139 
   3140         // Remove the current chip, so drag-and-drop will result in a move.
   3141         // TODO (phamm): consider readd this chip if it's dropped outside a target.
   3142         removeChip(currentChip);
   3143     }
   3144 
   3145     /**
   3146      * Handles drag event.
   3147      */
   3148     @Override
   3149     public boolean onDragEvent(@NonNull DragEvent event) {
   3150         switch (event.getAction()) {
   3151             case DragEvent.ACTION_DRAG_STARTED:
   3152                 // Only handle plain text drag and drop.
   3153                 return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
   3154             case DragEvent.ACTION_DRAG_ENTERED:
   3155                 requestFocus();
   3156                 return true;
   3157             case DragEvent.ACTION_DROP:
   3158                 handlePasteClip(event.getClipData());
   3159                 return true;
   3160         }
   3161         return false;
   3162     }
   3163 
   3164     /**
   3165      * Drag shadow for a {@link DrawableRecipientChip}.
   3166      */
   3167     private final class RecipientChipShadow extends DragShadowBuilder {
   3168         private final DrawableRecipientChip mChip;
   3169 
   3170         public RecipientChipShadow(DrawableRecipientChip chip) {
   3171             mChip = chip;
   3172         }
   3173 
   3174         @Override
   3175         public void onProvideShadowMetrics(@NonNull Point shadowSize,
   3176                 @NonNull Point shadowTouchPoint) {
   3177             Rect rect = mChip.getBounds();
   3178             shadowSize.set(rect.width(), rect.height());
   3179             shadowTouchPoint.set(rect.centerX(), rect.centerY());
   3180         }
   3181 
   3182         @Override
   3183         public void onDrawShadow(@NonNull Canvas canvas) {
   3184             mChip.draw(canvas);
   3185         }
   3186     }
   3187 
   3188     private void showCopyDialog(final String address) {
   3189         final Context context = getContext();
   3190         if (!mAttachedToWindow || context == null || !(context instanceof Activity)) {
   3191             return;
   3192         }
   3193 
   3194         final DialogFragment fragment = CopyDialog.newInstance(address);
   3195         fragment.show(((Activity) context).getFragmentManager(), CopyDialog.TAG);
   3196     }
   3197 
   3198     @Override
   3199     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
   3200         // Do nothing.
   3201         return false;
   3202     }
   3203 
   3204     @Override
   3205     public void onShowPress(MotionEvent e) {
   3206         // Do nothing.
   3207     }
   3208 
   3209     @Override
   3210     public boolean onSingleTapUp(MotionEvent e) {
   3211         // Do nothing.
   3212         return false;
   3213     }
   3214 
   3215     protected boolean isPhoneQuery() {
   3216         return getAdapter() != null
   3217                 && getAdapter().getQueryType() == BaseRecipientAdapter.QUERY_TYPE_PHONE;
   3218     }
   3219 
   3220     @Override
   3221     public BaseRecipientAdapter getAdapter() {
   3222         return (BaseRecipientAdapter) super.getAdapter();
   3223     }
   3224 
   3225     /**
   3226      * Append a new {@link RecipientEntry} to the end of the recipient chips, leaving any
   3227      * unfinished text at the end.
   3228      */
   3229     public void appendRecipientEntry(final RecipientEntry entry) {
   3230         clearComposingText();
   3231 
   3232         final Editable editable = getText();
   3233         int chipInsertionPoint = 0;
   3234 
   3235         // Find the end of last chip and see if there's any unchipified text.
   3236         final DrawableRecipientChip[] recips = getSortedRecipients();
   3237         if (recips != null && recips.length > 0) {
   3238             final DrawableRecipientChip last = recips[recips.length - 1];
   3239             // The chip will be inserted at the end of last chip + 1. All the unfinished text after
   3240             // the insertion point will be kept untouched.
   3241             chipInsertionPoint = editable.getSpanEnd(last) + 1;
   3242         }
   3243 
   3244         final CharSequence chip = createChip(entry);
   3245         if (chip != null) {
   3246             editable.insert(chipInsertionPoint, chip);
   3247         }
   3248     }
   3249 
   3250     /**
   3251      * Remove all chips matching the given RecipientEntry.
   3252      */
   3253     public void removeRecipientEntry(final RecipientEntry entry) {
   3254         final DrawableRecipientChip[] recips = getText()
   3255                 .getSpans(0, getText().length(), DrawableRecipientChip.class);
   3256 
   3257         for (final DrawableRecipientChip recipient : recips) {
   3258             final RecipientEntry existingEntry = recipient.getEntry();
   3259             if (existingEntry != null && existingEntry.isValid() &&
   3260                     existingEntry.isSamePerson(entry)) {
   3261                 removeChip(recipient);
   3262             }
   3263         }
   3264     }
   3265 
   3266     public void setAlternatePopupAnchor(View v) {
   3267         mAlternatePopupAnchor = v;
   3268     }
   3269 
   3270     @Override
   3271     public void setVisibility(int visibility) {
   3272         super.setVisibility(visibility);
   3273 
   3274         if (visibility != GONE && mRequiresShrinkWhenNotGone) {
   3275             mRequiresShrinkWhenNotGone = false;
   3276             mHandler.post(mDelayedShrink);
   3277         }
   3278     }
   3279 
   3280     private static class ChipBitmapContainer {
   3281         Bitmap bitmap;
   3282         // information used for positioning the loaded icon
   3283         boolean loadIcon = true;
   3284         float left;
   3285         float top;
   3286         float right;
   3287         float bottom;
   3288     }
   3289 }
   3290