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