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