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