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