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