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