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