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