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