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