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