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