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