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