RecipientEditTextView.java revision 6ed7ded9deabbc92ed8341cf922673fd4626ba7e
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ex.chips;
18
19import android.app.Dialog;
20import android.content.ClipData;
21import android.content.ClipDescription;
22import android.content.ClipboardManager;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.DialogInterface.OnDismissListener;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
28import android.graphics.Canvas;
29import android.graphics.Matrix;
30import android.graphics.Rect;
31import android.graphics.RectF;
32import android.graphics.drawable.BitmapDrawable;
33import android.graphics.drawable.Drawable;
34import android.os.AsyncTask;
35import android.os.Handler;
36import android.os.Message;
37import android.os.Parcelable;
38import android.text.Editable;
39import android.text.InputType;
40import android.text.Layout;
41import android.text.Spannable;
42import android.text.SpannableString;
43import android.text.SpannableStringBuilder;
44import android.text.Spanned;
45import android.text.TextPaint;
46import android.text.TextUtils;
47import android.text.TextWatcher;
48import android.text.method.QwertyKeyListener;
49import android.text.style.ImageSpan;
50import android.text.util.Rfc822Token;
51import android.text.util.Rfc822Tokenizer;
52import android.util.AttributeSet;
53import android.util.Log;
54import android.view.ActionMode;
55import android.view.ActionMode.Callback;
56import android.view.GestureDetector;
57import android.view.KeyEvent;
58import android.view.LayoutInflater;
59import android.view.Menu;
60import android.view.MenuItem;
61import android.view.MotionEvent;
62import android.view.View;
63import android.view.View.OnClickListener;
64import android.view.ViewParent;
65import android.widget.AdapterView;
66import android.widget.AdapterView.OnItemClickListener;
67import android.widget.ListAdapter;
68import android.widget.ListPopupWindow;
69import android.widget.ListView;
70import android.widget.MultiAutoCompleteTextView;
71import android.widget.PopupWindow;
72import android.widget.ScrollView;
73import android.widget.TextView;
74
75import java.util.ArrayList;
76import java.util.Arrays;
77import java.util.Collection;
78import java.util.Collections;
79import java.util.Comparator;
80import java.util.HashMap;
81import java.util.HashSet;
82import java.util.Set;
83
84/**
85 * RecipientEditTextView is an auto complete text view for use with applications
86 * that use the new Chips UI for addressing a message to recipients.
87 */
88public class RecipientEditTextView extends MultiAutoCompleteTextView implements
89        OnItemClickListener, Callback, RecipientAlternatesAdapter.OnCheckedItemChangedListener,
90        GestureDetector.OnGestureListener, OnDismissListener, OnClickListener,
91        PopupWindow.OnDismissListener {
92
93    private static final char COMMIT_CHAR_COMMA = ',';
94
95    private static final char COMMIT_CHAR_SEMICOLON = ';';
96
97    private static final char COMMIT_CHAR_SPACE = ' ';
98
99    private static final String TAG = "RecipientEditTextView";
100
101    private static int DISMISS = "dismiss".hashCode();
102
103    private static final long DISMISS_DELAY = 300;
104
105    // TODO: get correct number/ algorithm from with UX.
106    private static final int CHIP_LIMIT = 2;
107
108    private static int sSelectedTextColor = -1;
109
110    // Resources for displaying chips.
111    private Drawable mChipBackground = null;
112
113    private Drawable mChipDelete = null;
114
115    private Drawable mInvalidChipBackground;
116
117    private Drawable mChipBackgroundPressed;
118
119    private float mChipHeight;
120
121    private float mChipFontSize;
122
123    private int mChipPadding;
124
125    private Tokenizer mTokenizer;
126
127    private Validator mValidator;
128
129    private RecipientChip mSelectedChip;
130
131    private int mAlternatesLayout;
132
133    private Bitmap mDefaultContactPhoto;
134
135    private ImageSpan mMoreChip;
136
137    private TextView mMoreItem;
138
139    private final ArrayList<String> mPendingChips = new ArrayList<String>();
140
141    private Handler mHandler;
142
143    private int mPendingChipsCount = 0;
144
145    private ListPopupWindow mAlternatesPopup;
146
147    private ListPopupWindow mAddressPopup;
148
149    private ArrayList<RecipientChip> mTemporaryRecipients;
150
151    private ArrayList<RecipientChip> mRemovedSpans;
152
153    private boolean mShouldShrink = true;
154
155    // Chip copy fields.
156    private GestureDetector mGestureDetector;
157
158    private Dialog mCopyDialog;
159
160    private int mCopyViewRes;
161
162    private String mCopyAddress;
163
164    /**
165     * Used with {@link #mAlternatesPopup}. Handles clicks to alternate addresses for a
166     * selected chip.
167     */
168    private OnItemClickListener mAlternatesListener;
169
170    private int mCheckedItem;
171
172    private TextWatcher mTextWatcher;
173
174    // Obtain the enclosing scroll view, if it exists, so that the view can be
175    // scrolled to show the last line of chips content.
176    private ScrollView mScrollView;
177
178    private boolean mTriedGettingScrollView;
179
180    private final Runnable mAddTextWatcher = new Runnable() {
181        @Override
182        public void run() {
183            if (mTextWatcher == null) {
184                mTextWatcher = new RecipientTextWatcher();
185                addTextChangedListener(mTextWatcher);
186            }
187        }
188    };
189
190    private IndividualReplacementTask mIndividualReplacements;
191
192    private Runnable mHandlePendingChips = new Runnable() {
193
194        @Override
195        public void run() {
196            handlePendingChips();
197        }
198
199    };
200
201    private Runnable mDelayedShrink = new Runnable() {
202
203        @Override
204        public void run() {
205            shrink();
206        }
207
208    };
209
210    public RecipientEditTextView(Context context, AttributeSet attrs) {
211        super(context, attrs);
212        if (sSelectedTextColor == -1) {
213            sSelectedTextColor = context.getResources().getColor(android.R.color.white);
214        }
215        mAlternatesPopup = new ListPopupWindow(context);
216        mAlternatesPopup.setOnDismissListener(this);
217        mAddressPopup = new ListPopupWindow(context);
218        mAddressPopup.setOnDismissListener(this);
219        mCopyDialog = new Dialog(context);
220        mAlternatesListener = new OnItemClickListener() {
221            @Override
222            public void onItemClick(AdapterView<?> adapterView,View view, int position,
223                    long rowId) {
224                mAlternatesPopup.setOnItemClickListener(null);
225                setEnabled(true);
226                replaceChip(mSelectedChip, ((RecipientAlternatesAdapter) adapterView.getAdapter())
227                        .getRecipientEntry(position));
228                Message delayed = Message.obtain(mHandler, DISMISS);
229                delayed.obj = mAlternatesPopup;
230                mHandler.sendMessageDelayed(delayed, DISMISS_DELAY);
231                clearComposingText();
232            }
233        };
234        setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
235        setOnItemClickListener(this);
236        setCustomSelectionActionModeCallback(this);
237        mHandler = new Handler() {
238            @Override
239            public void handleMessage(Message msg) {
240                if (msg.what == DISMISS) {
241                    ((ListPopupWindow) msg.obj).dismiss();
242                    return;
243                }
244                super.handleMessage(msg);
245            }
246        };
247        mTextWatcher = new RecipientTextWatcher();
248        addTextChangedListener(mTextWatcher);
249        mGestureDetector = new GestureDetector(context, this);
250    }
251
252    /*package*/ RecipientChip getLastChip() {
253        RecipientChip last = null;
254        RecipientChip[] chips = getSortedRecipients();
255        if (chips != null && chips.length > 0) {
256            last = chips[chips.length - 1];
257        }
258        return last;
259    }
260
261    @Override
262    public void onSelectionChanged(int start, int end) {
263        // When selection changes, see if it is inside the chips area.
264        // If so, move the cursor back after the chips again.
265        RecipientChip last = getLastChip();
266        if (last != null) {
267            // Grab the last chip and set the cursor to after it.
268            setSelection(Math.min(getSpannable().getSpanEnd(last) + 1, getText().length()));
269        }
270        super.onSelectionChanged(start, end);
271    }
272
273    @Override
274    public void onRestoreInstanceState(Parcelable state) {
275        if (!TextUtils.isEmpty(getText())) {
276            super.onRestoreInstanceState(null);
277        } else {
278            super.onRestoreInstanceState(state);
279        }
280    }
281
282    @Override
283    public Parcelable onSaveInstanceState() {
284        // If the user changes orientation while they are editing, just roll back the selection.
285        clearSelectedChip();
286        return super.onSaveInstanceState();
287    }
288
289    /**
290     * Convenience method: Append the specified text slice to the TextView's
291     * display buffer, upgrading it to BufferType.EDITABLE if it was
292     * not already editable. Commas are excluded as they are added automatically
293     * by the view.
294     */
295    @Override
296    public void append(CharSequence text, int start, int end) {
297        // We don't care about watching text changes while appending.
298        if (mTextWatcher != null) {
299            removeTextChangedListener(mTextWatcher);
300        }
301        super.append(text, start, end);
302        if (!TextUtils.isEmpty(text) && TextUtils.getTrimmedLength(text) > 0) {
303            final String displayString = (String) text;
304            int seperatorPos = displayString.indexOf(COMMIT_CHAR_COMMA);
305            if (seperatorPos != 0 && !TextUtils.isEmpty(displayString)
306                    && TextUtils.getTrimmedLength(displayString) > 0) {
307                mPendingChipsCount++;
308                mPendingChips.add((String)text);
309            }
310        }
311        // Put a message on the queue to make sure we ALWAYS handle pending chips.
312        if (mPendingChipsCount > 0) {
313            postHandlePendingChips();
314        }
315        mHandler.post(mAddTextWatcher);
316    }
317
318    @Override
319    public void onFocusChanged(boolean hasFocus, int direction, Rect previous) {
320        super.onFocusChanged(hasFocus, direction, previous);
321        if (!hasFocus) {
322            shrink();
323        } else {
324            expand();
325            scrollLineIntoView(getLineCount());
326        }
327    }
328
329    @Override
330    public void performValidation() {
331        // Do nothing. Chips handles its own validation.
332    }
333
334    private void shrink() {
335        if (mSelectedChip != null
336                && mSelectedChip.getEntry().getContactId() != RecipientEntry.INVALID_CONTACT) {
337            clearSelectedChip();
338        } else {
339            if (getWidth() <= 0) {
340                // We don't have the width yet which means the view hasn't been drawn yet
341                // and there is no reason to attempt to commit chips yet.
342                // This focus lost must be the result of an orientation change
343                // or an initial rendering.
344                // Re-post the shrink for later.
345                mHandler.removeCallbacks(mDelayedShrink);
346                mHandler.post(mDelayedShrink);
347                return;
348            }
349            // Reset any pending chips as they would have been handled
350            // when the field lost focus.
351            if (mPendingChipsCount > 0) {
352                postHandlePendingChips();
353            } else {
354                Editable editable = getText();
355                int end = getSelectionEnd();
356                int start = mTokenizer.findTokenStart(editable, end);
357                RecipientChip[] chips = getSpannable().getSpans(start, end, RecipientChip.class);
358                if ((chips == null || chips.length == 0)) {
359                    Editable text = getText();
360                    int whatEnd = mTokenizer.findTokenEnd(text, start);
361                    // This token was already tokenized, so skip past the ending token.
362                    if (whatEnd < text.length() && text.charAt(whatEnd) == ',') {
363                        whatEnd++;
364                    }
365                    // In the middle of chip; treat this as an edit
366                    // and commit the whole token.
367                    int selEnd = getSelectionEnd();
368                    if (whatEnd != selEnd && whatEnd != editable.toString().trim().length()) {
369                        handleEdit(start, whatEnd);
370                    } else {
371                        commitChip(start, end, editable);
372                    }
373                }
374            }
375            mHandler.post(mAddTextWatcher);
376        }
377        createMoreChip();
378    }
379
380    private void expand() {
381        removeMoreChip();
382        setCursorVisible(true);
383        Editable text = getText();
384        setSelection(text != null && text.length() > 0 ? text.length() : 0);
385        // If there are any temporary chips, try replacing them now that the user
386        // has expanded the field.
387        if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0) {
388            new RecipientReplacementTask().execute();
389            mTemporaryRecipients = null;
390        }
391    }
392
393    private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) {
394        paint.setTextSize(mChipFontSize);
395        if (maxWidth <= 0 && Log.isLoggable(TAG, Log.DEBUG)) {
396            Log.d(TAG, "Max width is negative: " + maxWidth);
397        }
398        return TextUtils.ellipsize(text, paint, maxWidth,
399                TextUtils.TruncateAt.END);
400    }
401
402    private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint, Layout layout) {
403        // Ellipsize the text so that it takes AT MOST the entire width of the
404        // autocomplete text entry area. Make sure to leave space for padding
405        // on the sides.
406        int height = (int) mChipHeight;
407        int deleteWidth = height;
408        float[] widths = new float[1];
409        paint.getTextWidths(" ", widths);
410        CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
411                calculateAvailableWidth(true) - deleteWidth - widths[0]);
412
413        // Make sure there is a minimum chip width so the user can ALWAYS
414        // tap a chip without difficulty.
415        int width = Math.max(deleteWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
416                ellipsizedText.length()))
417                + (mChipPadding * 2) + deleteWidth);
418
419        // Create the background of the chip.
420        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
421        Canvas canvas = new Canvas(tmpBitmap);
422        if (mChipBackgroundPressed != null) {
423            mChipBackgroundPressed.setBounds(0, 0, width, height);
424            mChipBackgroundPressed.draw(canvas);
425            paint.setColor(sSelectedTextColor);
426            // Vertically center the text in the chip.
427            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding,
428                    getTextYOffset((String) ellipsizedText, paint, height), paint);
429            // Make the delete a square.
430            Rect backgroundPadding = new Rect();
431            mChipBackgroundPressed.getPadding(backgroundPadding);
432            mChipDelete.setBounds(width - deleteWidth + backgroundPadding.left,
433                    0 + backgroundPadding.top,
434                    width - backgroundPadding.right,
435                    height - backgroundPadding.bottom);
436            mChipDelete.draw(canvas);
437        } else {
438            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
439        }
440        return tmpBitmap;
441    }
442
443
444    private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint, Layout layout) {
445        // Ellipsize the text so that it takes AT MOST the entire width of the
446        // autocomplete text entry area. Make sure to leave space for padding
447        // on the sides.
448        int height = (int) mChipHeight;
449        int iconWidth = height;
450        float[] widths = new float[1];
451        paint.getTextWidths(" ", widths);
452        CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
453                calculateAvailableWidth(false) - iconWidth - widths[0]);
454        // Make sure there is a minimum chip width so the user can ALWAYS
455        // tap a chip without difficulty.
456        int width = Math.max(iconWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
457                ellipsizedText.length()))
458                + (mChipPadding * 2) + iconWidth);
459
460        // Create the background of the chip.
461        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
462        Canvas canvas = new Canvas(tmpBitmap);
463        Drawable background = getChipBackground(contact);
464        if (background != null) {
465            background.setBounds(0, 0, width, height);
466            background.draw(canvas);
467
468            // Don't draw photos for recipients that have been typed in.
469            if (contact.getContactId() != RecipientEntry.INVALID_CONTACT) {
470                byte[] photoBytes = contact.getPhotoBytes();
471                // There may not be a photo yet if anything but the first contact address
472                // was selected.
473                if (photoBytes == null && contact.getPhotoThumbnailUri() != null) {
474                    // TODO: cache this in the recipient entry?
475                    ((BaseRecipientAdapter) getAdapter()).fetchPhoto(contact, contact
476                            .getPhotoThumbnailUri());
477                    photoBytes = contact.getPhotoBytes();
478                }
479
480                Bitmap photo;
481                if (photoBytes != null) {
482                    photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
483                } else {
484                    // TODO: can the scaled down default photo be cached?
485                    photo = mDefaultContactPhoto;
486                }
487                // Draw the photo on the left side.
488                if (photo != null) {
489                    RectF src = new RectF(0, 0, photo.getWidth(), photo.getHeight());
490                    Rect backgroundPadding = new Rect();
491                    mChipBackground.getPadding(backgroundPadding);
492                    RectF dst = new RectF(width - iconWidth + backgroundPadding.left,
493                            0 + backgroundPadding.top,
494                            width - backgroundPadding.right,
495                            height - backgroundPadding.bottom);
496                    Matrix matrix = new Matrix();
497                    matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);
498                    canvas.drawBitmap(photo, matrix, paint);
499                }
500            } else {
501                // Don't leave any space for the icon. It isn't being drawn.
502                iconWidth = 0;
503            }
504            paint.setColor(getContext().getResources().getColor(android.R.color.black));
505            // Vertically center the text in the chip.
506            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding,
507                    getTextYOffset((String)ellipsizedText, paint, height), paint);
508        } else {
509            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
510        }
511        return tmpBitmap;
512    }
513
514    /**
515     * Get the background drawable for a RecipientChip.
516     */
517    // Visible for testing.
518    /*package*/ Drawable getChipBackground(RecipientEntry contact) {
519        return (mValidator != null && mValidator.isValid(contact.getDestination())) ?
520                mChipBackground : mInvalidChipBackground;
521    }
522
523    private float getTextYOffset(String text, TextPaint paint, int height) {
524        Rect bounds = new Rect();
525        paint.getTextBounds((String)text, 0, text.length(), bounds);
526        int textHeight = bounds.bottom - bounds.top  - (int)paint.descent();
527        return height - ((height - textHeight) / 2);
528    }
529
530    private RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
531            throws NullPointerException {
532        if (mChipBackground == null) {
533            throw new NullPointerException(
534                    "Unable to render any chips as setChipDimensions was not called.");
535        }
536        Layout layout = getLayout();
537
538        TextPaint paint = getPaint();
539        float defaultSize = paint.getTextSize();
540        int defaultColor = paint.getColor();
541
542        Bitmap tmpBitmap;
543        if (pressed) {
544            tmpBitmap = createSelectedChip(contact, paint, layout);
545
546        } else {
547            tmpBitmap = createUnselectedChip(contact, paint, layout);
548        }
549
550        // Pass the full text, un-ellipsized, to the chip.
551        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
552        result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
553        RecipientChip recipientChip = new RecipientChip(result, contact, offset);
554        // Return text to the original size.
555        paint.setTextSize(defaultSize);
556        paint.setColor(defaultColor);
557        return recipientChip;
558    }
559
560    /**
561     * Calculate the bottom of the line the chip will be located on using:
562     * 1) which line the chip appears on
563     * 2) the height of a chip
564     * 3) padding built into the edit text view
565     */
566    private int calculateOffsetFromBottom(int line) {
567        // Line offsets start at zero.
568        int actualLine = getLineCount() - (line + 1);
569        return -((actualLine * ((int) mChipHeight) + getPaddingBottom()) + getPaddingTop())
570                + getDropDownVerticalOffset();
571    }
572
573    /**
574     * Get the max amount of space a chip can take up. The formula takes into
575     * account the width of the EditTextView, any view padding, and padding
576     * that will be added to the chip.
577     */
578    private float calculateAvailableWidth(boolean pressed) {
579        return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2);
580    }
581
582    /**
583     * Set all chip dimensions and resources. This has to be done from the
584     * application as this is a static library.
585     * @param chipBackground
586     * @param chipBackgroundPressed
587     * @param invalidChip
588     * @param chipDelete
589     * @param defaultContact
590     * @param moreResource
591     * @param alternatesLayout
592     * @param chipHeight
593     * @param padding Padding around the text in a chip
594     * @param chipFontSize
595     * @param copyViewRes
596     */
597    public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
598            Drawable invalidChip, Drawable chipDelete, Bitmap defaultContact, int moreResource,
599            int alternatesLayout, float chipHeight, float padding,
600            float chipFontSize, int copyViewRes) {
601        mChipBackground = chipBackground;
602        mChipBackgroundPressed = chipBackgroundPressed;
603        mChipDelete = chipDelete;
604        mChipPadding = (int) padding;
605        mAlternatesLayout = alternatesLayout;
606        mDefaultContactPhoto = defaultContact;
607        mMoreItem = (TextView) LayoutInflater.from(getContext()).inflate(moreResource, null);
608        mChipHeight = chipHeight;
609        mChipFontSize = chipFontSize;
610        mInvalidChipBackground = invalidChip;
611        mCopyViewRes = copyViewRes;
612    }
613
614    // Visible for testing.
615    /* package */ void setMoreItem(TextView moreItem) {
616        mMoreItem = moreItem;
617    }
618
619
620    // Visible for testing.
621    /* package */ void setChipBackground(Drawable chipBackground) {
622        mChipBackground = chipBackground;
623    }
624
625    // Visible for testing.
626    /* package */ void setChipHeight(int height) {
627        mChipHeight = height;
628    }
629
630    /**
631     * Set whether to shrink the recipients field such that at most
632     * one line of recipients chips are shown when the field loses
633     * focus. By default, the number of displayed recipients will be
634     * limited and a "more" chip will be shown when focus is lost.
635     * @param shrink
636     */
637    public void setOnFocusListShrinkRecipients(boolean shrink) {
638        mShouldShrink = shrink;
639    }
640
641    @Override
642    public void onSizeChanged(int width, int height, int oldw, int oldh) {
643        super.onSizeChanged(width, height, oldw, oldh);
644        if (width != 0 && height != 0) {
645            if (mPendingChipsCount > 0) {
646                postHandlePendingChips();
647            } else {
648                checkChipWidths();
649            }
650        }
651        // Try to find the scroll view parent, if it exists.
652        if (mScrollView == null && !mTriedGettingScrollView) {
653            ViewParent parent = getParent();
654            while (parent != null && !(parent instanceof ScrollView)) {
655                parent = parent.getParent();
656            }
657            if (parent != null) {
658                mScrollView = (ScrollView) parent;
659            }
660            mTriedGettingScrollView = true;
661        }
662    }
663
664    private void postHandlePendingChips() {
665        mHandler.removeCallbacks(mHandlePendingChips);
666        mHandler.post(mHandlePendingChips);
667    }
668
669    private void checkChipWidths() {
670        // Check the widths of the associated chips.
671        RecipientChip[] chips = getSortedRecipients();
672        if (chips != null) {
673            Rect bounds;
674            for (RecipientChip chip : chips) {
675                bounds = chip.getDrawable().getBounds();
676                if (getWidth() > 0 && bounds.right - bounds.left > getWidth()) {
677                    // Need to redraw that chip.
678                    replaceChip(chip, chip.getEntry());
679                }
680            }
681        }
682    }
683
684    private void handlePendingChips() {
685        if (getWidth() <= 0) {
686            // The widget has not been sized yet.
687            // This will be called as a result of onSizeChanged
688            // at a later point.
689            return;
690        }
691
692        if (mPendingChipsCount <= 0) {
693            return;
694        }
695
696        synchronized (mPendingChips) {
697            Editable editable = getText();
698            // Tokenize!
699            for (int i = 0; i < mPendingChips.size(); i++) {
700                String current = mPendingChips.get(i);
701                int tokenStart = editable.toString().indexOf(current);
702                int tokenEnd = tokenStart + current.length();
703                if (tokenStart >= 0) {
704                    // When we have a valid token, include it with the token
705                    // to the left.
706                    if (tokenEnd < editable.length() - 2
707                            && editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) {
708                        tokenEnd++;
709                    }
710                    createReplacementChip(tokenStart, tokenEnd, editable);
711                }
712                mPendingChipsCount--;
713            }
714            sanitizeEnd();
715            if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0
716                    && mTemporaryRecipients.size() <= RecipientAlternatesAdapter.MAX_LOOKUPS) {
717                if (hasFocus() || mTemporaryRecipients.size() < CHIP_LIMIT) {
718                    new RecipientReplacementTask().execute();
719                    mTemporaryRecipients = null;
720                } else {
721                    // Create the "more" chip
722                    mIndividualReplacements = new IndividualReplacementTask();
723                    mIndividualReplacements.execute(new ArrayList<RecipientChip>(
724                            mTemporaryRecipients.subList(0, CHIP_LIMIT)));
725
726                    createMoreChip();
727                }
728            } else {
729                // There are too many recipients to look up, so just fall back
730                // to showing addresses for all of them.
731                mTemporaryRecipients = null;
732                createMoreChip();
733            }
734            mPendingChipsCount = 0;
735            mPendingChips.clear();
736        }
737    }
738
739    /**
740     * Remove any characters after the last valid chip.
741     */
742    // Visible for testing.
743    /*package*/ void sanitizeEnd() {
744        // Find the last chip; eliminate any commit characters after it.
745        RecipientChip[] chips = getSortedRecipients();
746        if (chips != null && chips.length > 0) {
747            int end;
748            ImageSpan lastSpan;
749            mMoreChip = getMoreChip();
750            if (mMoreChip != null) {
751                lastSpan = mMoreChip;
752            } else {
753                lastSpan = getLastChip();
754            }
755            end = getSpannable().getSpanEnd(lastSpan);
756            Editable editable = getText();
757            int length = editable.length();
758            if (length > end) {
759                // See what characters occur after that and eliminate them.
760                if (Log.isLoggable(TAG, Log.DEBUG)) {
761                    Log.d(TAG, "There were extra characters after the last tokenizable entry."
762                            + editable);
763                }
764                editable.delete(end + 1, length);
765            }
766        }
767    }
768
769    /**
770     * Create a chip that represents just the email address of a recipient. At some later
771     * point, this chip will be attached to a real contact entry, if one exists.
772     */
773    private void createReplacementChip(int tokenStart, int tokenEnd, Editable editable) {
774        if (alreadyHasChip(tokenStart, tokenEnd)) {
775            // There is already a chip present at this location.
776            // Don't recreate it.
777            return;
778        }
779        String token = editable.toString().substring(tokenStart, tokenEnd);
780        int commitCharIndex = token.trim().lastIndexOf(COMMIT_CHAR_COMMA);
781        if (commitCharIndex == token.length() - 1) {
782            token = token.substring(0, token.length() - 1);
783        }
784        RecipientEntry entry = createTokenizedEntry(token);
785        if (entry != null) {
786            String destText = createAddressText(entry);
787            // Always leave a blank space at the end of a chip.
788            int textLength = destText.length() - 1;
789            SpannableString chipText = new SpannableString(destText);
790            int end = getSelectionEnd();
791            int start = mTokenizer.findTokenStart(getText(), end);
792            RecipientChip chip = null;
793            try {
794                chip = constructChipSpan(entry, start, false);
795                chipText.setSpan(chip, 0, textLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
796            } catch (NullPointerException e) {
797                Log.e(TAG, e.getMessage(), e);
798            }
799            editable.replace(tokenStart, tokenEnd, chipText);
800            // Add this chip to the list of entries "to replace"
801            if (chip != null) {
802                if (mTemporaryRecipients == null) {
803                    mTemporaryRecipients = new ArrayList<RecipientChip>();
804                }
805                chip.setOriginalText(chipText.toString());
806                mTemporaryRecipients.add(chip);
807            }
808        }
809    }
810
811    private RecipientEntry createTokenizedEntry(String token) {
812        if (TextUtils.isEmpty(token)) {
813            return null;
814        }
815        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token);
816        String display = null;
817        if (isValid(token) && tokens != null && tokens.length > 0) {
818            // If we can get a name from tokenizing, then generate an entry from
819            // this.
820            display = tokens[0].getName();
821            if (!TextUtils.isEmpty(display)) {
822                return RecipientEntry.constructGeneratedEntry(display, token);
823            } else {
824                display = tokens[0].getAddress();
825                if (!TextUtils.isEmpty(display)) {
826                    return RecipientEntry.constructFakeEntry(display);
827                }
828            }
829        }
830        // Unable to validate the token or to create a valid token from it.
831        // Just create a chip the user can edit.
832        String validatedToken = null;
833        if (mValidator != null && !mValidator.isValid(token)) {
834            // Try fixing up the entry using the validator.
835            validatedToken = mValidator.fixText(token).toString();
836            if (!TextUtils.isEmpty(validatedToken)) {
837                if (validatedToken.contains(token)) {
838                    // protect against the case of a validator with a null domain,
839                    // which doesn't add a domain to the token
840                    Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(validatedToken);
841                    if (tokenized.length > 0) {
842                        validatedToken = tokenized[0].getAddress();
843                    }
844                } else {
845                    // We ran into a case where the token was invalid and removed
846                    // by the validator. In this case, just use the original token
847                    // and let the user sort out the error chip.
848                    validatedToken = null;
849                }
850            }
851        }
852        // Otherwise, fallback to just creating an editable email address chip.
853        return RecipientEntry
854                .constructFakeEntry(!TextUtils.isEmpty(validatedToken) ? validatedToken : token);
855    }
856
857    private boolean isValid(String text) {
858        return mValidator == null ? true : mValidator.isValid(text);
859    }
860
861    private String tokenizeAddress(String destination) {
862        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination);
863        if (tokens != null && tokens.length > 0) {
864            return tokens[0].getAddress();
865        }
866        return destination;
867    }
868
869    @Override
870    public void setTokenizer(Tokenizer tokenizer) {
871        mTokenizer = tokenizer;
872        super.setTokenizer(mTokenizer);
873    }
874
875    @Override
876    public void setValidator(Validator validator) {
877        mValidator = validator;
878        super.setValidator(validator);
879    }
880
881    /**
882     * We cannot use the default mechanism for replaceText. Instead,
883     * we override onItemClickListener so we can get all the associated
884     * contact information including display text, address, and id.
885     */
886    @Override
887    protected void replaceText(CharSequence text) {
888        return;
889    }
890
891    /**
892     * Dismiss any selected chips when the back key is pressed.
893     */
894    @Override
895    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
896        if (keyCode == KeyEvent.KEYCODE_BACK) {
897            clearSelectedChip();
898        }
899        return super.onKeyPreIme(keyCode, event);
900    }
901
902    /**
903     * Monitor key presses in this view to see if the user types
904     * any commit keys, which consist of ENTER, TAB, or DPAD_CENTER.
905     * If the user has entered text that has contact matches and types
906     * a commit key, create a chip from the topmost matching contact.
907     * If the user has entered text that has no contact matches and types
908     * a commit key, then create a chip from the text they have entered.
909     */
910    @Override
911    public boolean onKeyUp(int keyCode, KeyEvent event) {
912        switch (keyCode) {
913            case KeyEvent.KEYCODE_ENTER:
914            case KeyEvent.KEYCODE_DPAD_CENTER:
915                if (event.hasNoModifiers()) {
916                    if (commitDefault()) {
917                        return true;
918                    }
919                    if (mSelectedChip != null) {
920                        clearSelectedChip();
921                        return true;
922                    } else if (focusNext()) {
923                        return true;
924                    }
925                }
926                break;
927            case KeyEvent.KEYCODE_TAB:
928                if (event.hasNoModifiers()) {
929                    if (mSelectedChip != null) {
930                        clearSelectedChip();
931                    } else {
932                        commitDefault();
933                    }
934                    if (focusNext()) {
935                        return true;
936                    }
937                }
938        }
939        return super.onKeyUp(keyCode, event);
940    }
941
942    private boolean focusNext() {
943        View next = focusSearch(View.FOCUS_DOWN);
944        if (next != null) {
945            next.requestFocus();
946            return true;
947        }
948        return false;
949    }
950
951    /**
952     * Create a chip from the default selection. If the popup is showing, the
953     * default is the first item in the popup suggestions list. Otherwise, it is
954     * whatever the user had typed in. End represents where the the tokenizer
955     * should search for a token to turn into a chip.
956     * @return If a chip was created from a real contact.
957     */
958    private boolean commitDefault() {
959        Editable editable = getText();
960        int end = getSelectionEnd();
961        int start = mTokenizer.findTokenStart(editable, end);
962
963        if (shouldCreateChip(start, end)) {
964            int whatEnd = mTokenizer.findTokenEnd(getText(), start);
965            // In the middle of chip; treat this as an edit
966            // and commit the whole token.
967            if (whatEnd != getSelectionEnd()) {
968                handleEdit(start, whatEnd);
969                return true;
970            }
971            return commitChip(start, end , editable);
972        }
973        return false;
974    }
975
976    private void commitByCharacter() {
977        Editable editable = getText();
978        int end = getSelectionEnd();
979        int start = mTokenizer.findTokenStart(editable, end);
980        if (shouldCreateChip(start, end)) {
981            commitChip(start, end, editable);
982        }
983        setSelection(getText().length());
984    }
985
986    private boolean commitChip(int start, int end, Editable editable) {
987        ListAdapter adapter = getAdapter();
988        if (adapter != null && adapter.getCount() > 0 && enoughToFilter()
989                && end == getSelectionEnd()) {
990            // choose the first entry.
991            submitItemAtPosition(0);
992            dismissDropDown();
993            return true;
994        } else {
995            int tokenEnd = mTokenizer.findTokenEnd(editable, start);
996            if (editable.length() > tokenEnd + 1) {
997                char charAt = editable.charAt(tokenEnd + 1);
998                if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) {
999                    tokenEnd++;
1000                }
1001            }
1002            String text = editable.toString().substring(start, tokenEnd).trim();
1003            clearComposingText();
1004            if (text != null && text.length() > 0 && !text.equals(" ")) {
1005                RecipientEntry entry = createTokenizedEntry(text);
1006                if (entry != null) {
1007                    QwertyKeyListener.markAsReplaced(editable, start, end, "");
1008                    CharSequence chipText = createChip(entry, false);
1009                    if (chipText != null && start > -1 && end > -1) {
1010                        editable.replace(start, end, chipText);
1011                    }
1012                }
1013                // Only dismiss the dropdown if it is related to the text we
1014                // just committed.
1015                // For paste, it may not be as there are possibly multiple
1016                // tokens being added.
1017                if (end == getSelectionEnd()) {
1018                    dismissDropDown();
1019                }
1020                sanitizeBetween();
1021                return true;
1022            }
1023        }
1024        return false;
1025    }
1026
1027    // Visible for testing.
1028    /* package */ void sanitizeBetween() {
1029        // Find the last chip.
1030        RecipientChip[] recips = getSortedRecipients();
1031        if (recips != null && recips.length > 0) {
1032            RecipientChip last = recips[recips.length - 1];
1033            RecipientChip beforeLast = null;
1034            if (recips.length > 1) {
1035                beforeLast = recips[recips.length - 2];
1036            }
1037            int startLooking = 0;
1038            int end = getSpannable().getSpanStart(last);
1039            if (beforeLast != null) {
1040                startLooking = getSpannable().getSpanEnd(beforeLast);
1041                Editable text = getText();
1042                if (startLooking == -1 || startLooking > text.length() - 1) {
1043                    // There is nothing after this chip.
1044                    return;
1045                }
1046                if (text.charAt(startLooking) == ' ') {
1047                    startLooking++;
1048                }
1049            }
1050            if (startLooking >= 0 && end >= 0 && startLooking != end) {
1051                getText().delete(startLooking, end);
1052            }
1053        }
1054    }
1055
1056    private boolean shouldCreateChip(int start, int end) {
1057        return hasFocus() && enoughToFilter() && !alreadyHasChip(start, end);
1058    }
1059
1060    private boolean alreadyHasChip(int start, int end) {
1061        RecipientChip[] chips = getSpannable().getSpans(start, end, RecipientChip.class);
1062        if ((chips == null || chips.length == 0)) {
1063            return false;
1064        }
1065        return true;
1066    }
1067
1068    private void handleEdit(int start, int end) {
1069        if (start == -1 || end == -1) {
1070            // This chip no longer exists in the field.
1071            dismissDropDown();
1072            return;
1073        }
1074        // This is in the middle of a chip, so select out the whole chip
1075        // and commit it.
1076        Editable editable = getText();
1077        setSelection(end);
1078        String text = getText().toString().substring(start, end);
1079        if (!TextUtils.isEmpty(text)) {
1080            RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
1081            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1082            CharSequence chipText = createChip(entry, false);
1083            if (chipText != null) {
1084                editable.replace(start, getSelectionEnd(), chipText);
1085            }
1086        }
1087        dismissDropDown();
1088    }
1089
1090    /**
1091     * If there is a selected chip, delegate the key events
1092     * to the selected chip.
1093     */
1094    @Override
1095    public boolean onKeyDown(int keyCode, KeyEvent event) {
1096        if (mSelectedChip != null && keyCode == KeyEvent.KEYCODE_DEL) {
1097            if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
1098                mAlternatesPopup.dismiss();
1099            }
1100            removeChip(mSelectedChip);
1101        }
1102
1103        if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
1104            return true;
1105        }
1106
1107        return super.onKeyDown(keyCode, event);
1108    }
1109
1110    // Visible for testing.
1111    /* package */ Spannable getSpannable() {
1112        return getText();
1113    }
1114
1115    private int getChipStart(RecipientChip chip) {
1116        return getSpannable().getSpanStart(chip);
1117    }
1118
1119    private int getChipEnd(RecipientChip chip) {
1120        return getSpannable().getSpanEnd(chip);
1121    }
1122
1123    /**
1124     * Instead of filtering on the entire contents of the edit box,
1125     * this subclass method filters on the range from
1126     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
1127     * if the length of that range meets or exceeds {@link #getThreshold}
1128     * and makes sure that the range is not already a Chip.
1129     */
1130    @Override
1131    protected void performFiltering(CharSequence text, int keyCode) {
1132        if (enoughToFilter() && !isCompletedToken(text)) {
1133            int end = getSelectionEnd();
1134            int start = mTokenizer.findTokenStart(text, end);
1135            // If this is a RecipientChip, don't filter
1136            // on its contents.
1137            Spannable span = getSpannable();
1138            RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
1139            if (chips != null && chips.length > 0) {
1140                return;
1141            }
1142        }
1143        super.performFiltering(text, keyCode);
1144    }
1145
1146    // Visible for testing.
1147    /*package*/ boolean isCompletedToken(CharSequence text) {
1148        if (TextUtils.isEmpty(text)) {
1149            return false;
1150        }
1151        // Check to see if this is a completed token before filtering.
1152        int end = text.length();
1153        int start = mTokenizer.findTokenStart(text, end);
1154        String token = text.toString().substring(start, end).trim();
1155        if (!TextUtils.isEmpty(token)) {
1156            char atEnd = token.charAt(token.length() - 1);
1157            return atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON;
1158        }
1159        return false;
1160    }
1161
1162    private void clearSelectedChip() {
1163        if (mSelectedChip != null) {
1164            unselectChip(mSelectedChip);
1165            mSelectedChip = null;
1166        }
1167        setCursorVisible(true);
1168    }
1169
1170    /**
1171     * Monitor touch events in the RecipientEditTextView.
1172     * If the view does not have focus, any tap on the view
1173     * will just focus the view. If the view has focus, determine
1174     * if the touch target is a recipient chip. If it is and the chip
1175     * is not selected, select it and clear any other selected chips.
1176     * If it isn't, then select that chip.
1177     */
1178    @Override
1179    public boolean onTouchEvent(MotionEvent event) {
1180        if (!isFocused()) {
1181            // Ignore any chip taps until this view is focused.
1182            return super.onTouchEvent(event);
1183        }
1184        boolean handled = super.onTouchEvent(event);
1185        int action = event.getAction();
1186        boolean chipWasSelected = false;
1187        if (mSelectedChip == null) {
1188            mGestureDetector.onTouchEvent(event);
1189        }
1190        if (mCopyAddress == null && action == MotionEvent.ACTION_UP) {
1191            float x = event.getX();
1192            float y = event.getY();
1193            int offset = putOffsetInRange(getOffsetForPosition(x, y));
1194            RecipientChip currentChip = findChip(offset);
1195            if (currentChip != null) {
1196                if (action == MotionEvent.ACTION_UP) {
1197                    if (mSelectedChip != null && mSelectedChip != currentChip) {
1198                        clearSelectedChip();
1199                        mSelectedChip = selectChip(currentChip);
1200                    } else if (mSelectedChip == null) {
1201                        setSelection(getText().length());
1202                        commitDefault();
1203                        mSelectedChip = selectChip(currentChip);
1204                    } else {
1205                        onClick(mSelectedChip, offset, x, y);
1206                    }
1207                }
1208                chipWasSelected = true;
1209                handled = true;
1210            } else if (mSelectedChip != null
1211                    && mSelectedChip.getContactId() == RecipientEntry.INVALID_CONTACT) {
1212                chipWasSelected = true;
1213            }
1214        }
1215        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
1216            clearSelectedChip();
1217        }
1218        return handled;
1219    }
1220
1221    private void scrollLineIntoView(int line) {
1222        if (mScrollView != null) {
1223            mScrollView.scrollBy(0, calculateOffsetFromBottom(line));
1224        }
1225    }
1226
1227    private void showAlternates(RecipientChip currentChip, ListPopupWindow alternatesPopup,
1228            int width, Context context) {
1229        int line = getLayout().getLineForOffset(getChipStart(currentChip));
1230        int bottom = calculateOffsetFromBottom(line);
1231        // Align the alternates popup with the left side of the View,
1232        // regardless of the position of the chip tapped.
1233        alternatesPopup.setWidth(width);
1234        setEnabled(false);
1235        alternatesPopup.setAnchorView(this);
1236        alternatesPopup.setVerticalOffset(bottom);
1237        alternatesPopup.setAdapter(createAlternatesAdapter(currentChip));
1238        alternatesPopup.setOnItemClickListener(mAlternatesListener);
1239        // Clear the checked item.
1240        mCheckedItem = -1;
1241        alternatesPopup.show();
1242        ListView listView = alternatesPopup.getListView();
1243        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
1244        // Checked item would be -1 if the adapter has not
1245        // loaded the view that should be checked yet. The
1246        // variable will be set correctly when onCheckedItemChanged
1247        // is called in a separate thread.
1248        if (mCheckedItem != -1) {
1249            listView.setItemChecked(mCheckedItem, true);
1250            mCheckedItem = -1;
1251        }
1252    }
1253
1254    // Dismiss listener for alterns and single address popup.
1255    @Override
1256    public void onDismiss() {
1257        setEnabled(true);
1258    }
1259
1260    private ListAdapter createAlternatesAdapter(RecipientChip chip) {
1261        return new RecipientAlternatesAdapter(getContext(), chip.getContactId(), chip.getDataId(),
1262                mAlternatesLayout, this);
1263    }
1264
1265    private ListAdapter createSingleAddressAdapter(RecipientChip currentChip) {
1266        return new SingleRecipientArrayAdapter(getContext(), mAlternatesLayout, currentChip
1267                .getEntry());
1268    }
1269
1270    @Override
1271    public void onCheckedItemChanged(int position) {
1272        ListView listView = mAlternatesPopup.getListView();
1273        if (listView != null && listView.getCheckedItemCount() == 0) {
1274            listView.setItemChecked(position, true);
1275        }
1276        mCheckedItem = position;
1277    }
1278
1279    // TODO: This algorithm will need a lot of tweaking after more people have used
1280    // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
1281    // what comes before the finger.
1282    private int putOffsetInRange(int o) {
1283        int offset = o;
1284        Editable text = getText();
1285        int length = text.length();
1286        // Remove whitespace from end to find "real end"
1287        int realLength = length;
1288        for (int i = length - 1; i >= 0; i--) {
1289            if (text.charAt(i) == ' ') {
1290                realLength--;
1291            } else {
1292                break;
1293            }
1294        }
1295
1296        // If the offset is beyond or at the end of the text,
1297        // leave it alone.
1298        if (offset >= realLength) {
1299            return offset;
1300        }
1301        Editable editable = getText();
1302        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
1303            // Keep walking backward!
1304            offset--;
1305        }
1306        return offset;
1307    }
1308
1309    private int findText(Editable text, int offset) {
1310        if (text.charAt(offset) != ' ') {
1311            return offset;
1312        }
1313        return -1;
1314    }
1315
1316    private RecipientChip findChip(int offset) {
1317        RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class);
1318        // Find the chip that contains this offset.
1319        for (int i = 0; i < chips.length; i++) {
1320            RecipientChip chip = chips[i];
1321            int start = getChipStart(chip);
1322            int end = getChipEnd(chip);
1323            if (offset >= start && offset <= end) {
1324                return chip;
1325            }
1326        }
1327        return null;
1328    }
1329
1330    // Visible for testing.
1331    // Use this method to generate text to add to the list of addresses.
1332    /*package*/ String createAddressText(RecipientEntry entry) {
1333        String display = entry.getDisplayName();
1334        String address = entry.getDestination();
1335        if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
1336            display = null;
1337        }
1338        if (address != null) {
1339            // Tokenize out the address in case the address already
1340            // contained the username as well.
1341            Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(address);
1342            if (tokenized != null && tokenized.length > 0) {
1343                address = tokenized[0].getAddress();
1344            }
1345        }
1346        Rfc822Token token = new Rfc822Token(display, address, null);
1347        String trimmedDisplayText = token.toString().trim();
1348        int index = trimmedDisplayText.indexOf(",");
1349        return index < trimmedDisplayText.length() - 1 ? (String) mTokenizer
1350                .terminateToken(trimmedDisplayText) : trimmedDisplayText;
1351    }
1352
1353    // Visible for testing.
1354    // Use this method to generate text to display in a chip.
1355    /*package*/ String createChipDisplayText(RecipientEntry entry) {
1356        String display = entry.getDisplayName();
1357        String address = entry.getDestination();
1358        if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
1359            display = null;
1360        }
1361        if (address != null) {
1362            // Tokenize out the address in case the address already
1363            // contained the username as well.
1364            Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(address);
1365            if (tokenized != null && tokenized.length > 0) {
1366                address = tokenized[0].getAddress();
1367            }
1368        }
1369        if (!TextUtils.isEmpty(display)) {
1370            return display;
1371        } else if (!TextUtils.isEmpty(address)){
1372            return address;
1373        } else {
1374            return new Rfc822Token(display, address, null).toString();
1375        }
1376    }
1377
1378    private CharSequence createChip(RecipientEntry entry, boolean pressed) {
1379        String displayText = createAddressText(entry);
1380        if (TextUtils.isEmpty(displayText)) {
1381            return null;
1382        }
1383        // Always leave a blank space at the end of a chip.
1384        int textLength = displayText.length()-1;
1385        SpannableString chipText = new SpannableString(displayText);
1386        int end = getSelectionEnd();
1387        int start = mTokenizer.findTokenStart(getText(), end);
1388        try {
1389            RecipientChip chip = constructChipSpan(entry, start, pressed);
1390            chipText.setSpan(chip, 0, textLength,
1391                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1392            chip.setOriginalText(chipText.toString());
1393        } catch (NullPointerException e) {
1394            Log.e(TAG, e.getMessage(), e);
1395            return null;
1396        }
1397
1398        return chipText;
1399    }
1400
1401    /**
1402     * When an item in the suggestions list has been clicked, create a chip from the
1403     * contact information of the selected item.
1404     */
1405    @Override
1406    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1407        submitItemAtPosition(position);
1408    }
1409
1410    private void submitItemAtPosition(int position) {
1411        RecipientEntry entry = createValidatedEntry(
1412                (RecipientEntry)getAdapter().getItem(position));
1413        if (entry == null) {
1414            return;
1415        }
1416        clearComposingText();
1417
1418        int end = getSelectionEnd();
1419        int start = mTokenizer.findTokenStart(getText(), end);
1420
1421        Editable editable = getText();
1422        QwertyKeyListener.markAsReplaced(editable, start, end, "");
1423        CharSequence chip = createChip(entry, false);
1424        if (chip != null) {
1425            editable.replace(start, end, chip);
1426        }
1427        sanitizeBetween();
1428    }
1429
1430    private RecipientEntry createValidatedEntry(RecipientEntry item) {
1431        if (item == null) {
1432            return null;
1433        }
1434        final RecipientEntry entry;
1435        // If the display name and the address are the same, or if this is a
1436        // valid contact, but the destination is invalid, then make this a fake
1437        // recipient that is editable.
1438        String destination = item.getDestination();
1439        if (RecipientEntry.isCreatedRecipient(item.getContactId())
1440                && (TextUtils.isEmpty(item.getDisplayName())
1441                        || TextUtils.equals(item.getDisplayName(), destination)
1442                        || (mValidator != null && !mValidator.isValid(destination)))) {
1443            entry = RecipientEntry.constructFakeEntry(destination);
1444        } else {
1445            entry = item;
1446        }
1447        return entry;
1448    }
1449
1450    /** Returns a collection of contact Id for each chip inside this View. */
1451    /* package */ Collection<Long> getContactIds() {
1452        final Set<Long> result = new HashSet<Long>();
1453        RecipientChip[] chips = getSortedRecipients();
1454        if (chips != null) {
1455            for (RecipientChip chip : chips) {
1456                result.add(chip.getContactId());
1457            }
1458        }
1459        return result;
1460    }
1461
1462
1463    /** Returns a collection of data Id for each chip inside this View. May be null. */
1464    /* package */ Collection<Long> getDataIds() {
1465        final Set<Long> result = new HashSet<Long>();
1466        RecipientChip [] chips = getSortedRecipients();
1467        if (chips != null) {
1468            for (RecipientChip chip : chips) {
1469                result.add(chip.getDataId());
1470            }
1471        }
1472        return result;
1473    }
1474
1475    // Visible for testing.
1476    /* package */RecipientChip[] getSortedRecipients() {
1477        RecipientChip[] recips = getSpannable()
1478                .getSpans(0, getText().length(), RecipientChip.class);
1479        ArrayList<RecipientChip> recipientsList = new ArrayList<RecipientChip>(Arrays
1480                .asList(recips));
1481        final Spannable spannable = getSpannable();
1482        Collections.sort(recipientsList, new Comparator<RecipientChip>() {
1483
1484            @Override
1485            public int compare(RecipientChip first, RecipientChip second) {
1486                int firstStart = spannable.getSpanStart(first);
1487                int secondStart = spannable.getSpanStart(second);
1488                if (firstStart < secondStart) {
1489                    return -1;
1490                } else if (firstStart > secondStart) {
1491                    return 1;
1492                } else {
1493                    return 0;
1494                }
1495            }
1496        });
1497        return recipientsList.toArray(new RecipientChip[recipientsList.size()]);
1498    }
1499
1500    @Override
1501    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1502        return false;
1503    }
1504
1505    @Override
1506    public void onDestroyActionMode(ActionMode mode) {
1507    }
1508
1509    @Override
1510    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1511        return false;
1512    }
1513
1514    /**
1515     * No chips are selectable.
1516     */
1517    @Override
1518    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
1519        return false;
1520    }
1521
1522    // Visible for testing.
1523    /* package */ImageSpan getMoreChip() {
1524        MoreImageSpan[] moreSpans = getSpannable().getSpans(0, getText().length(),
1525                MoreImageSpan.class);
1526        return moreSpans != null && moreSpans.length > 0 ? moreSpans[0] : null;
1527    }
1528
1529    /**
1530     * Create the more chip. The more chip is text that replaces any chips that
1531     * do not fit in the pre-defined available space when the
1532     * RecipientEditTextView loses focus.
1533     */
1534    // Visible for testing.
1535    /* package */ void createMoreChip() {
1536        if (!mShouldShrink) {
1537            return;
1538        }
1539
1540        ImageSpan[] tempMore = getSpannable().getSpans(0, getText().length(), MoreImageSpan.class);
1541        if (tempMore.length > 0) {
1542            getSpannable().removeSpan(tempMore[0]);
1543        }
1544        RecipientChip[] recipients = getSortedRecipients();
1545        if (recipients == null || recipients.length <= CHIP_LIMIT) {
1546            mMoreChip = null;
1547            return;
1548        }
1549        Spannable spannable = getSpannable();
1550        int numRecipients = recipients.length;
1551        int overage = numRecipients - CHIP_LIMIT;
1552        String moreText = String.format(mMoreItem.getText().toString(), overage);
1553        TextPaint morePaint = new TextPaint(getPaint());
1554        morePaint.setTextSize(mMoreItem.getTextSize());
1555        morePaint.setColor(mMoreItem.getCurrentTextColor());
1556        int width = (int)morePaint.measureText(moreText) + mMoreItem.getPaddingLeft()
1557                + mMoreItem.getPaddingRight();
1558        int height = getLineHeight();
1559        Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1560        Canvas canvas = new Canvas(drawable);
1561        int adjustedHeight = height;
1562        Layout layout = getLayout();
1563        if (layout != null) {
1564            adjustedHeight -= layout.getLineDescent(0);
1565        }
1566        canvas.drawText(moreText, 0, moreText.length(), 0, adjustedHeight, morePaint);
1567
1568        Drawable result = new BitmapDrawable(getResources(), drawable);
1569        result.setBounds(0, 0, width, height);
1570        MoreImageSpan moreSpan = new MoreImageSpan(result);
1571        // Remove the overage chips.
1572        if (recipients == null || recipients.length == 0) {
1573            Log.w(TAG,
1574                    "We have recipients. Tt should not be possible to have zero RecipientChips.");
1575            mMoreChip = null;
1576            return;
1577        }
1578        mRemovedSpans = new ArrayList<RecipientChip>();
1579        int totalReplaceStart = 0;
1580        int totalReplaceEnd = 0;
1581        Editable text = getText();
1582        for (int i = numRecipients - overage; i < recipients.length; i++) {
1583            mRemovedSpans.add(recipients[i]);
1584            if (i == numRecipients - overage) {
1585                totalReplaceStart = spannable.getSpanStart(recipients[i]);
1586            }
1587            if (i == recipients.length - 1) {
1588                totalReplaceEnd = spannable.getSpanEnd(recipients[i]);
1589            }
1590            if (mTemporaryRecipients == null || !mTemporaryRecipients.contains(recipients[i])) {
1591                int spanStart = spannable.getSpanStart(recipients[i]);
1592                int spanEnd = spannable.getSpanEnd(recipients[i]);
1593                recipients[i].setOriginalText(text.toString().substring(spanStart, spanEnd));
1594            }
1595            spannable.removeSpan(recipients[i]);
1596        }
1597        int end = Math.max(totalReplaceStart, totalReplaceEnd);
1598        int start = Math.min(totalReplaceStart, totalReplaceEnd);
1599        SpannableString chipText = new SpannableString(text.subSequence(start, end));
1600        chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1601        text.replace(start, end, chipText);
1602        mMoreChip = moreSpan;
1603    }
1604
1605    /**
1606     * Replace the more chip, if it exists, with all of the recipient chips it had
1607     * replaced when the RecipientEditTextView gains focus.
1608     */
1609    // Visible for testing.
1610    /*package*/ void removeMoreChip() {
1611        if (mMoreChip != null) {
1612            Spannable span = getSpannable();
1613            span.removeSpan(mMoreChip);
1614            mMoreChip = null;
1615            // Re-add the spans that were removed.
1616            if (mRemovedSpans != null && mRemovedSpans.size() > 0) {
1617                // Recreate each removed span.
1618                RecipientChip[] recipients = getSortedRecipients();
1619                // Start the search for tokens after the last currently visible
1620                // chip.
1621                if (recipients == null || recipients.length == 0) {
1622                    return;
1623                }
1624                int end = span.getSpanEnd(recipients[recipients.length - 1]);
1625                Editable editable = getText();
1626                for (RecipientChip chip : mRemovedSpans) {
1627                    int chipStart;
1628                    int chipEnd;
1629                    String token;
1630                    // Need to find the location of the chip, again.
1631                    token = (String) chip.getOriginalText();
1632                    // As we find the matching recipient for the remove spans,
1633                    // reduce the size of the string we need to search.
1634                    // That way, if there are duplicates, we always find the correct
1635                    // recipient.
1636                    chipStart = editable.toString().indexOf(token, end);
1637                    end = chipEnd = Math.min(editable.length(), chipStart + token.length());
1638                    // Only set the span if we found a matching token.
1639                    if (chipStart != -1) {
1640                        editable.setSpan(chip, chipStart, chipEnd,
1641                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1642                    }
1643                }
1644                mRemovedSpans.clear();
1645            }
1646        }
1647    }
1648
1649    /**
1650     * Show specified chip as selected. If the RecipientChip is just an email address,
1651     * selecting the chip will take the contents of the chip and place it at
1652     * the end of the RecipientEditTextView for inline editing. If the
1653     * RecipientChip is a complete contact, then selecting the chip
1654     * will change the background color of the chip, show the delete icon,
1655     * and a popup window with the address in use highlighted and any other
1656     * alternate addresses for the contact.
1657     * @param currentChip Chip to select.
1658     * @return A RecipientChip in the selected state or null if the chip
1659     * just contained an email address.
1660     */
1661    private RecipientChip selectChip(RecipientChip currentChip) {
1662        if (currentChip.getContactId() == RecipientEntry.INVALID_CONTACT) {
1663            CharSequence text = currentChip.getValue();
1664            Editable editable = getText();
1665            removeChip(currentChip);
1666            editable.append(text);
1667            setCursorVisible(true);
1668            setSelection(editable.length());
1669            return new RecipientChip(null, RecipientEntry.constructFakeEntry((String) text), -1);
1670        } else if (currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT) {
1671            int start = getChipStart(currentChip);
1672            int end = getChipEnd(currentChip);
1673            getSpannable().removeSpan(currentChip);
1674            RecipientChip newChip;
1675            try {
1676                newChip = constructChipSpan(currentChip.getEntry(), start, true);
1677            } catch (NullPointerException e) {
1678                Log.e(TAG, e.getMessage(), e);
1679                return null;
1680            }
1681            Editable editable = getText();
1682            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1683            if (start == -1 || end == -1) {
1684                Log.d(TAG, "The chip being selected no longer exists but should.");
1685            } else {
1686                editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1687            }
1688            newChip.setSelected(true);
1689            if (newChip.getEntry().getContactId() == RecipientEntry.INVALID_CONTACT) {
1690                scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
1691            }
1692            showAddress(newChip, mAddressPopup, getWidth(), getContext());
1693            setCursorVisible(false);
1694            return newChip;
1695        } else {
1696            int start = getChipStart(currentChip);
1697            int end = getChipEnd(currentChip);
1698            getSpannable().removeSpan(currentChip);
1699            RecipientChip newChip;
1700            try {
1701                newChip = constructChipSpan(currentChip.getEntry(), start, true);
1702            } catch (NullPointerException e) {
1703                Log.e(TAG, e.getMessage(), e);
1704                return null;
1705            }
1706            Editable editable = getText();
1707            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1708            if (start == -1 || end == -1) {
1709                Log.d(TAG, "The chip being selected no longer exists but should.");
1710            } else {
1711                editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1712            }
1713            newChip.setSelected(true);
1714            if (newChip.getEntry().getContactId() == RecipientEntry.INVALID_CONTACT) {
1715                scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
1716            }
1717            showAlternates(newChip, mAlternatesPopup, getWidth(), getContext());
1718            setCursorVisible(false);
1719            return newChip;
1720        }
1721    }
1722
1723
1724    private void showAddress(final RecipientChip currentChip, final ListPopupWindow popup,
1725            int width, Context context) {
1726        int line = getLayout().getLineForOffset(getChipStart(currentChip));
1727        int bottom = calculateOffsetFromBottom(line);
1728        // Align the alternates popup with the left side of the View,
1729        // regardless of the position of the chip tapped.
1730        setEnabled(false);
1731        popup.setWidth(width);
1732        popup.setAnchorView(this);
1733        popup.setVerticalOffset(bottom);
1734        popup.setAdapter(createSingleAddressAdapter(currentChip));
1735        popup.setOnItemClickListener(new OnItemClickListener() {
1736            @Override
1737            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1738                unselectChip(currentChip);
1739                popup.dismiss();
1740            }
1741        });
1742        popup.show();
1743        ListView listView = popup.getListView();
1744        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
1745        listView.setItemChecked(0, true);
1746    }
1747
1748    /**
1749     * Remove selection from this chip. Unselecting a RecipientChip will render
1750     * the chip without a delete icon and with an unfocused background. This
1751     * is called when the RecipientChip no longer has focus.
1752     */
1753    private void unselectChip(RecipientChip chip) {
1754        int start = getChipStart(chip);
1755        int end = getChipEnd(chip);
1756        Editable editable = getText();
1757        mSelectedChip = null;
1758        if (start == -1 || end == -1) {
1759            Log.w(TAG,
1760                    "The chip doesn't exist or may be a chip a user was editing");
1761            setSelection(editable.length());
1762            commitDefault();
1763        } else {
1764            getSpannable().removeSpan(chip);
1765            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1766            editable.removeSpan(chip);
1767            try {
1768                editable.setSpan(constructChipSpan(chip.getEntry(), start, false), start, end,
1769                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1770            } catch (NullPointerException e) {
1771                Log.e(TAG, e.getMessage(), e);
1772            }
1773        }
1774        setCursorVisible(true);
1775        setSelection(editable.length());
1776        if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
1777            mAlternatesPopup.dismiss();
1778        }
1779    }
1780
1781    /**
1782     * Return whether a touch event was inside the delete target of
1783     * a selected chip. It is in the delete target if:
1784     * 1) the x and y points of the event are within the
1785     * delete assset.
1786     * 2) the point tapped would have caused a cursor to appear
1787     * right after the selected chip.
1788     * @return boolean
1789     */
1790    private boolean isInDelete(RecipientChip chip, int offset, float x, float y) {
1791        // Figure out the bounds of this chip and whether or not
1792        // the user clicked in the X portion.
1793        return chip.isSelected() && offset == getChipEnd(chip);
1794    }
1795
1796    /**
1797     * Remove the chip and any text associated with it from the RecipientEditTextView.
1798     */
1799    // Visible for testing.
1800    /*pacakge*/ void removeChip(RecipientChip chip) {
1801        Spannable spannable = getSpannable();
1802        int spanStart = spannable.getSpanStart(chip);
1803        int spanEnd = spannable.getSpanEnd(chip);
1804        Editable text = getText();
1805        int toDelete = spanEnd;
1806        boolean wasSelected = chip == mSelectedChip;
1807        // Clear that there is a selected chip before updating any text.
1808        if (wasSelected) {
1809            mSelectedChip = null;
1810        }
1811        // Always remove trailing spaces when removing a chip.
1812        while (toDelete >= 0 && toDelete < text.length() && text.charAt(toDelete) == ' ') {
1813            toDelete++;
1814        }
1815        spannable.removeSpan(chip);
1816        text.delete(spanStart, toDelete);
1817        if (wasSelected) {
1818            clearSelectedChip();
1819        }
1820    }
1821
1822    /**
1823     * Replace this currently selected chip with a new chip
1824     * that uses the contact data provided.
1825     */
1826    // Visible for testing.
1827    /*package*/ void replaceChip(RecipientChip chip, RecipientEntry entry) {
1828        boolean wasSelected = chip == mSelectedChip;
1829        if (wasSelected) {
1830            mSelectedChip = null;
1831        }
1832        int start = getChipStart(chip);
1833        int end = getChipEnd(chip);
1834        getSpannable().removeSpan(chip);
1835        Editable editable = getText();
1836        CharSequence chipText = createChip(entry, false);
1837        if (chipText != null) {
1838            if (start == -1 || end == -1) {
1839                Log.e(TAG, "The chip to replace does not exist but should.");
1840                editable.insert(0, chipText);
1841            } else {
1842                if (!TextUtils.isEmpty(chipText)) {
1843                    // There may be a space to replace with this chip's new
1844                    // associated
1845                    // space. Check for it
1846                    int toReplace = end;
1847                    while (toReplace >= 0 && toReplace < editable.length()
1848                            && editable.charAt(toReplace) == ' ') {
1849                        toReplace++;
1850                    }
1851                    editable.replace(start, toReplace, chipText);
1852                }
1853            }
1854        }
1855        setCursorVisible(true);
1856        if (wasSelected) {
1857            clearSelectedChip();
1858        }
1859    }
1860
1861    /**
1862     * Handle click events for a chip. When a selected chip receives a click
1863     * event, see if that event was in the delete icon. If so, delete it.
1864     * Otherwise, unselect the chip.
1865     */
1866    public void onClick(RecipientChip chip, int offset, float x, float y) {
1867        if (chip.isSelected()) {
1868            if (isInDelete(chip, offset, x, y)) {
1869                removeChip(chip);
1870            } else {
1871                clearSelectedChip();
1872            }
1873        }
1874    }
1875
1876    private boolean chipsPending() {
1877        return mPendingChipsCount > 0 || (mRemovedSpans != null && mRemovedSpans.size() > 0);
1878    }
1879
1880    @Override
1881    public void removeTextChangedListener(TextWatcher watcher) {
1882        mTextWatcher = null;
1883        super.removeTextChangedListener(watcher);
1884    }
1885
1886    private class RecipientTextWatcher implements TextWatcher {
1887        @Override
1888        public void afterTextChanged(Editable s) {
1889            // If the text has been set to null or empty, make sure we remove
1890            // all the spans we applied.
1891            if (TextUtils.isEmpty(s)) {
1892                // Remove all the chips spans.
1893                Spannable spannable = getSpannable();
1894                RecipientChip[] chips = spannable.getSpans(0, getText().length(),
1895                        RecipientChip.class);
1896                for (RecipientChip chip : chips) {
1897                    spannable.removeSpan(chip);
1898                }
1899                if (mMoreChip != null) {
1900                    spannable.removeSpan(mMoreChip);
1901                }
1902                return;
1903            }
1904            // Get whether there are any recipients pending addition to the
1905            // view. If there are, don't do anything in the text watcher.
1906            if (chipsPending()) {
1907                return;
1908            }
1909            // If the user is editing a chip, don't clear it.
1910            if (mSelectedChip != null
1911                    && mSelectedChip.getContactId() != RecipientEntry.INVALID_CONTACT) {
1912                setCursorVisible(true);
1913                setSelection(getText().length());
1914                clearSelectedChip();
1915            }
1916            int length = s.length();
1917            // Make sure there is content there to parse and that it is
1918            // not just the commit character.
1919            if (length > 1) {
1920                char last;
1921                int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
1922                int len = length() - 1;
1923                if (end != len) {
1924                    last = s.charAt(end);
1925                } else {
1926                    last = s.charAt(len);
1927                }
1928                if (last == COMMIT_CHAR_SEMICOLON || last == COMMIT_CHAR_COMMA) {
1929                    commitByCharacter();
1930                } else if (last == COMMIT_CHAR_SPACE) {
1931                    // Check if this is a valid email address. If it is,
1932                    // commit it.
1933                    String text = getText().toString();
1934                    int tokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
1935                    String sub = text.substring(tokenStart, mTokenizer.findTokenEnd(text,
1936                            tokenStart));
1937                    if (!TextUtils.isEmpty(sub) && mValidator != null && mValidator.isValid(sub)) {
1938                        commitByCharacter();
1939                    }
1940                }
1941            }
1942        }
1943
1944        @Override
1945        public void onTextChanged(CharSequence s, int start, int before, int count) {
1946            // Do nothing.
1947        }
1948
1949        @Override
1950        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1951            // Do nothing.
1952        }
1953    }
1954
1955    @Override
1956    public boolean onTextContextMenuItem(int id) {
1957        if (id == android.R.id.paste) {
1958            removeTextChangedListener(mTextWatcher);
1959            ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
1960                    Context.CLIPBOARD_SERVICE);
1961            ClipData clip = clipboard.getPrimaryClip();
1962            if (clip != null
1963                    && clip.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
1964                for (int i = 0; i < clip.getItemCount(); i++) {
1965                    CharSequence paste = clip.getItemAt(i).getText();
1966                    if (paste != null) {
1967                        int start = getSelectionStart();
1968                        int end = getSelectionEnd();
1969                        Editable editable = getText();
1970                        if (start >= 0 && end >= 0 && start != end) {
1971                            editable.append(paste, start, end);
1972                        } else {
1973                            editable.insert(end, paste);
1974                        }
1975                        handlePasteAndReplace();
1976                    }
1977                }
1978            }
1979            mHandler.post(mAddTextWatcher);
1980            return true;
1981        }
1982        return super.onTextContextMenuItem(id);
1983    }
1984
1985    private void handlePasteAndReplace() {
1986        ArrayList<RecipientChip> created = handlePaste();
1987        if (created != null && created.size() > 0) {
1988            // Perform reverse lookups on the pasted contacts.
1989            IndividualReplacementTask replace = new IndividualReplacementTask();
1990            replace.execute(created);
1991        }
1992    }
1993
1994    // Visible for testing.
1995    /* package */ArrayList<RecipientChip> handlePaste() {
1996        String text = getText().toString();
1997        int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
1998        String lastAddress = text.substring(originalTokenStart);
1999        int tokenStart = originalTokenStart;
2000        int prevTokenStart = tokenStart;
2001        RecipientChip findChip = null;
2002        ArrayList<RecipientChip> created = new ArrayList<RecipientChip>();
2003        if (tokenStart != 0) {
2004            // There are things before this!
2005            while (tokenStart != 0 && findChip == null) {
2006                prevTokenStart = tokenStart;
2007                tokenStart = mTokenizer.findTokenStart(text, tokenStart);
2008                findChip = findChip(tokenStart);
2009            }
2010            if (tokenStart != originalTokenStart) {
2011                if (findChip != null) {
2012                    tokenStart = prevTokenStart;
2013                }
2014                int tokenEnd;
2015                RecipientChip createdChip;
2016                while (tokenStart < originalTokenStart) {
2017                    tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(text, tokenStart));
2018                    commitChip(tokenStart, tokenEnd, getText());
2019                    createdChip = findChip(tokenStart);
2020                    // +1 for the space at the end.
2021                    tokenStart = getSpannable().getSpanEnd(createdChip) + 1;
2022                    created.add(createdChip);
2023                }
2024            }
2025        }
2026        // Take a look at the last token. If the token has been completed with a
2027        // commit character, create a chip.
2028        if (isCompletedToken(lastAddress)) {
2029            Editable editable = getText();
2030            tokenStart = editable.toString().indexOf(lastAddress, originalTokenStart);
2031            commitChip(tokenStart, editable.length(), editable);
2032            created.add(findChip(tokenStart));
2033        }
2034        return created;
2035    }
2036
2037    // Visible for testing.
2038    /* package */int movePastTerminators(int tokenEnd) {
2039        if (tokenEnd >= length()) {
2040            return tokenEnd;
2041        }
2042        char atEnd = getText().toString().charAt(tokenEnd);
2043        if (atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON) {
2044            tokenEnd++;
2045        }
2046        // This token had not only an end token character, but also a space
2047        // separating it from the next token.
2048        if (tokenEnd < length() && getText().toString().charAt(tokenEnd) == ' ') {
2049            tokenEnd++;
2050        }
2051        return tokenEnd;
2052    }
2053
2054    private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> {
2055        private RecipientChip createFreeChip(RecipientEntry entry) {
2056            try {
2057                return constructChipSpan(entry, -1, false);
2058            } catch (NullPointerException e) {
2059                Log.e(TAG, e.getMessage(), e);
2060                return null;
2061            }
2062        }
2063
2064        @Override
2065        protected Void doInBackground(Void... params) {
2066            if (mIndividualReplacements != null) {
2067                mIndividualReplacements.cancel(true);
2068            }
2069            // For each chip in the list, look up the matching contact.
2070            // If there is a match, replace that chip with the matching
2071            // chip.
2072            final ArrayList<RecipientChip> originalRecipients = new ArrayList<RecipientChip>();
2073            RecipientChip[] existingChips = getSortedRecipients();
2074            for (int i = 0; i < existingChips.length; i++) {
2075                originalRecipients.add(existingChips[i]);
2076            }
2077            if (mRemovedSpans != null) {
2078                originalRecipients.addAll(mRemovedSpans);
2079            }
2080            String[] addresses = new String[originalRecipients.size()];
2081            for (int i = 0; i < originalRecipients.size(); i++) {
2082                addresses[i] = createAddressText(originalRecipients.get(i).getEntry());
2083            }
2084            HashMap<String, RecipientEntry> entries = RecipientAlternatesAdapter
2085                    .getMatchingRecipients(getContext(), addresses);
2086            final ArrayList<RecipientChip> replacements = new ArrayList<RecipientChip>();
2087            for (final RecipientChip temp : originalRecipients) {
2088                RecipientEntry entry = null;
2089                if (RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())
2090                        && getSpannable().getSpanStart(temp) != -1) {
2091                    // Replace this.
2092                    entry = createValidatedEntry(entries.get(tokenizeAddress(temp.getEntry()
2093                            .getDestination())));
2094                }
2095                if (entry != null) {
2096                    replacements.add(createFreeChip(entry));
2097                } else {
2098                    replacements.add(temp);
2099                }
2100            }
2101            if (replacements != null && replacements.size() > 0) {
2102                mHandler.post(new Runnable() {
2103                    @Override
2104                    public void run() {
2105                        SpannableStringBuilder text = new SpannableStringBuilder(getText()
2106                                .toString());
2107                        Editable oldText = getText();
2108                        int start, end;
2109                        int i = 0;
2110                        for (RecipientChip chip : originalRecipients) {
2111                            start = oldText.getSpanStart(chip);
2112                            if (start != -1) {
2113                                end = oldText.getSpanEnd(chip);
2114                                oldText.removeSpan(chip);
2115                                // Leave a spot for the space!
2116                                RecipientChip replacement = replacements.get(i);
2117                                text.setSpan(replacement, start, end,
2118                                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2119                                replacement.setOriginalText(text.toString().substring(start, end));
2120                            }
2121                            i++;
2122                        }
2123                        originalRecipients.clear();
2124                        setText(text);
2125                    }
2126                });
2127            }
2128            return null;
2129        }
2130    }
2131
2132    private class IndividualReplacementTask extends AsyncTask<Object, Void, Void> {
2133        @SuppressWarnings("unchecked")
2134        @Override
2135        protected Void doInBackground(Object... params) {
2136            // For each chip in the list, look up the matching contact.
2137            // If there is a match, replace that chip with the matching
2138            // chip.
2139            final ArrayList<RecipientChip> originalRecipients =
2140                (ArrayList<RecipientChip>) params[0];
2141            String[] addresses = new String[originalRecipients.size()];
2142            for (int i = 0; i < originalRecipients.size(); i++) {
2143                addresses[i] = createAddressText(originalRecipients.get(i).getEntry());
2144            }
2145            HashMap<String, RecipientEntry> entries = RecipientAlternatesAdapter
2146                    .getMatchingRecipients(getContext(), addresses);
2147            for (final RecipientChip temp : originalRecipients) {
2148                if (RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())
2149                        && getSpannable().getSpanStart(temp) != -1) {
2150                    // Replace this.
2151                    final RecipientEntry entry = createValidatedEntry(entries
2152                            .get(tokenizeAddress(temp.getEntry().getDestination()).toLowerCase()));
2153                    if (entry != null) {
2154                        mHandler.post(new Runnable() {
2155                            @Override
2156                            public void run() {
2157                                replaceChip(temp, entry);
2158                            }
2159                        });
2160                    }
2161                }
2162            }
2163            return null;
2164        }
2165    }
2166
2167
2168    /**
2169     * MoreImageSpan is a simple class created for tracking the existence of a
2170     * more chip across activity restarts/
2171     */
2172    private class MoreImageSpan extends ImageSpan {
2173        public MoreImageSpan(Drawable b) {
2174            super(b);
2175        }
2176    }
2177
2178    @Override
2179    public boolean onDown(MotionEvent e) {
2180        return false;
2181    }
2182
2183    @Override
2184    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
2185        // Do nothing.
2186        return false;
2187    }
2188
2189    @Override
2190    public void onLongPress(MotionEvent event) {
2191        if (mSelectedChip != null) {
2192            return;
2193        }
2194        float x = event.getX();
2195        float y = event.getY();
2196        int offset = putOffsetInRange(getOffsetForPosition(x, y));
2197        RecipientChip currentChip = findChip(offset);
2198        if (currentChip != null) {
2199            // Copy the selected chip email address.
2200            showCopyDialog(currentChip.getEntry().getDestination());
2201        }
2202    }
2203
2204    private void showCopyDialog(final String address) {
2205        mCopyAddress = address;
2206        mCopyDialog.setTitle(address);
2207        mCopyDialog.setContentView(mCopyViewRes);
2208        mCopyDialog.setCancelable(true);
2209        mCopyDialog.setCanceledOnTouchOutside(true);
2210        mCopyDialog.findViewById(android.R.id.button1).setOnClickListener(this);
2211        mCopyDialog.setOnDismissListener(this);
2212        mCopyDialog.show();
2213    }
2214
2215    @Override
2216    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
2217        // Do nothing.
2218        return false;
2219    }
2220
2221    @Override
2222    public void onShowPress(MotionEvent e) {
2223        // Do nothing.
2224    }
2225
2226    @Override
2227    public boolean onSingleTapUp(MotionEvent e) {
2228        // Do nothing.
2229        return false;
2230    }
2231
2232    @Override
2233    public void onDismiss(DialogInterface dialog) {
2234        mCopyAddress = null;
2235    }
2236
2237    @Override
2238    public void onClick(View v) {
2239        // Copy this to the clipboard.
2240        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
2241                Context.CLIPBOARD_SERVICE);
2242        clipboard.setPrimaryClip(ClipData.newPlainText("", mCopyAddress));
2243        mCopyDialog.dismiss();
2244    }
2245}
2246