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