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