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