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