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