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