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