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