RecipientEditTextView.java revision b86dcd5230ebcc57e5fc7a669c2304aca142dbf5
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ex.chips;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Rect;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
26import android.text.Editable;
27import android.text.Layout;
28import android.text.Spannable;
29import android.text.SpannableString;
30import android.text.Spanned;
31import android.text.TextPaint;
32import android.text.TextUtils;
33import android.text.TextWatcher;
34import android.text.method.QwertyKeyListener;
35import android.text.style.ImageSpan;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.ActionMode;
39import android.view.KeyEvent;
40import android.view.Menu;
41import android.view.MenuItem;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.ActionMode.Callback;
45import android.widget.AdapterView;
46import android.widget.AdapterView.OnItemClickListener;
47import android.widget.ListPopupWindow;
48import android.widget.MultiAutoCompleteTextView;
49
50import java.util.Collection;
51import java.util.HashSet;
52import java.util.Set;
53
54import java.util.ArrayList;
55
56/**
57 * RecipientEditTextView is an auto complete text view for use with applications
58 * that use the new Chips UI for addressing a message to recipients.
59 */
60public class RecipientEditTextView extends MultiAutoCompleteTextView
61    implements OnItemClickListener, Callback {
62
63    private static final String TAG = "RecipientEditTextView";
64
65    private Drawable mChipBackground = null;
66
67    private Drawable mChipDelete = null;
68
69    private int mChipPadding;
70
71    private Tokenizer mTokenizer;
72
73    private Drawable mChipBackgroundPressed;
74
75    private RecipientChip mSelectedChip;
76
77    private int mChipDeleteWidth;
78
79    private ArrayList<RecipientChip> mRecipients;
80
81    private int mAlternatesLayout;
82
83    private int mAlternatesSelectedLayout;
84
85    public RecipientEditTextView(Context context, AttributeSet attrs) {
86        super(context, attrs);
87        mRecipients = new ArrayList<RecipientChip>();
88        setOnItemClickListener(this);
89        setCustomSelectionActionModeCallback(this);
90        // When the user starts typing, make sure we unselect any selected
91        // chips.
92        addTextChangedListener(new TextWatcher() {
93            @Override
94            public void afterTextChanged(Editable s) {
95                // Do nothing.
96            }
97            @Override
98            public void onTextChanged(CharSequence s, int start, int before, int count) {
99                // Do nothing.
100            }
101            @Override
102            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
103                if (mSelectedChip != null) {
104                    clearSelectedChip();
105                    setSelection(getText().length());
106                }
107            }
108        });
109    }
110
111    @Override
112    public void onSelectionChanged(int start, int end) {
113        // When selection changes, see if it is inside the chips area.
114        // If so, move the cursor back after the chips again.
115        if (mRecipients != null && mRecipients.size() > 0) {
116            Spannable span = getSpannable();
117            RecipientChip[] chips = span.getSpans(start, getText().length(), RecipientChip.class);
118            if (chips != null && chips.length > 0) {
119                // Grab the last chip and set the cursor to after it.
120                setSelection(chips[chips.length - 1].getChipEnd() + 1);
121            }
122        } else {
123            super.onSelectionChanged(start, end);
124        }
125    }
126
127    public RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
128        throws NullPointerException {
129        if (mChipBackground == null) {
130            throw new NullPointerException
131                ("Unable to render any chips as setChipDimensions was not called.");
132        }
133        String text = contact.getDisplayName();
134        Layout layout = getLayout();
135        int line = layout.getLineForOffset(offset);
136        int lineTop = layout.getLineTop(line);
137
138        TextPaint paint = getPaint();
139        float defaultSize = paint.getTextSize();
140
141        // Reduce the size of the text slightly so that we can get the "look" of
142        // padding.
143        paint.setTextSize((float) (paint.getTextSize() * .9));
144
145        // Ellipsize the text so that it takes AT MOST the entire width of the
146        // autocomplete text entry area. Make sure to leave space for padding
147        // on the sides.
148        CharSequence ellipsizedText = TextUtils.ellipsize(text, paint,
149                calculateAvailableWidth(pressed), TextUtils.TruncateAt.END);
150
151        int height = getLineHeight();
152        int width = (int) Math.floor(paint.measureText(ellipsizedText, 0, ellipsizedText.length()))
153                + (mChipPadding * 2);
154        if (pressed) {
155            // Allow the delete icon to overtake the visible recipient name.
156            // This works since when the user has entered selected mode, they
157            // will also see a popup with the recipient name.
158            width += mChipDeleteWidth/2;
159        }
160
161        // Create the background of the chip.
162        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
163        Canvas canvas = new Canvas(tmpBitmap);
164        if (pressed) {
165            if (mChipBackgroundPressed != null) {
166                mChipBackgroundPressed.setBounds(0, 0, width, height);
167                mChipBackgroundPressed.draw(canvas);
168
169                // Align the display text with where the user enters text.
170                canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
171                        - layout.getLineDescent(line), paint);
172                mChipDelete.setBounds(width - mChipDeleteWidth, 0, width, height);
173                mChipDelete.draw(canvas);
174            } else {
175                Log.w(TAG,
176                        "Unable to draw a background for the chips as it was never set");
177            }
178        } else {
179            if (mChipBackground != null) {
180                mChipBackground.setBounds(0, 0, width, height);
181                mChipBackground.draw(canvas);
182
183                // Align the display text with where the user enters text.
184                canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
185                        - layout.getLineDescent(line), paint);
186            } else {
187                Log.w(TAG,
188                        "Unable to draw a background for the chips as it was never set");
189            }
190        }
191
192
193        // Get the location of the widget so we can properly offset
194        // the anchor for each chip.
195        int[] xy = new int[2];
196        getLocationOnScreen(xy);
197        // Pass the full text, un-ellipsized, to the chip.
198        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
199        result.setBounds(0, 0, width, height);
200        Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + width,
201                calculateLineBottom(xy[1], line));
202        RecipientChip recipientChip = new RecipientChip(result, contact, offset, bounds);
203
204        // Return text to the original size.
205        paint.setTextSize(defaultSize);
206
207        return recipientChip;
208    }
209
210    // The bottom of the line the chip will be located on is calculated by 4 factors:
211    // 1) which line the chip appears on
212    // 2) the height of a line in the autocomplete view
213    // 3) padding built into the edit text view will move the bottom position
214    // 4) the position of the autocomplete view on the screen, taking into account
215    // that any top padding will move this down visually
216    private int calculateLineBottom(int yOffset, int line) {
217        int bottomPadding = 0;
218        if (line == getLineCount() - 1) {
219            bottomPadding += getPaddingBottom();
220        }
221        return ((line + 1) * getLineHeight()) + (yOffset + getPaddingTop()) + bottomPadding;
222    }
223
224    // Get the max amount of space a chip can take up. The formula takes into
225    // account the width of the EditTextView, any view padding, and padding
226    // that will be added to the chip.
227    private float calculateAvailableWidth(boolean pressed) {
228        int paddingRight = 0;
229        if (pressed) {
230            paddingRight = mChipDeleteWidth;
231        }
232        return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2)
233                - paddingRight;
234    }
235
236    /**
237     * Set all chip dimensions and resources. This has to be done from the application
238     * as this is a static library.
239     * @param chipBackground drawable
240     * @param padding Padding around the text in a chip
241     * @param offset Offset between the chip and the dropdown of alternates
242     */
243    public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
244            Drawable chipDelete, int alternatesLayout, int alternatesSelectedLayout, float padding) {
245        mChipBackground = chipBackground;
246        mChipBackgroundPressed = chipBackgroundPressed;
247        mChipDelete = chipDelete;
248        mChipDeleteWidth = chipDelete.getIntrinsicWidth();
249        mChipPadding = (int) padding;
250        mAlternatesLayout = alternatesLayout;
251        mAlternatesSelectedLayout = alternatesSelectedLayout;
252    }
253
254    @Override
255    public void setTokenizer(Tokenizer tokenizer) {
256        mTokenizer = tokenizer;
257        super.setTokenizer(mTokenizer);
258    }
259
260    // We want to handle replacing text in the onItemClickListener
261    // so we can get all the associated contact information including
262    // display text, address, and id.
263    @Override
264    protected void replaceText(CharSequence text) {
265        return;
266    }
267
268    @Override
269    public boolean onKeyUp(int keyCode, KeyEvent event) {
270        switch (keyCode) {
271            case KeyEvent.KEYCODE_ENTER:
272            case KeyEvent.KEYCODE_DPAD_CENTER:
273            case KeyEvent.KEYCODE_TAB:
274                if (event.hasNoModifiers()) {
275                    if (isPopupShowing()) {
276                        // choose the first entry.
277                        submitItemAtPosition(0);
278                        dismissDropDown();
279                        return true;
280                    } else {
281                        int end = getSelectionEnd();
282                        int start = mTokenizer.findTokenStart(getText(), end);
283                        String text = getText().toString().substring(start, end);
284                        clearComposingText();
285
286                        Editable editable = getText();
287                        RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
288                        QwertyKeyListener.markAsReplaced(editable, start, end, "");
289                        editable.replace(start, end, createChip(entry));
290                        dismissDropDown();
291                    }
292                }
293        }
294        return super.onKeyUp(keyCode, event);
295    }
296
297    @Override
298    public boolean onKeyDown(int keyCode, KeyEvent event) {
299        if (mSelectedChip != null) {
300            mSelectedChip.onKeyDown(keyCode, event);
301        }
302
303        if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
304            return true;
305        }
306
307        return super.onKeyDown(keyCode, event);
308    }
309
310    private Spannable getSpannable() {
311        return (Spannable) getText();
312    }
313
314    /**
315     * Instead of filtering on the entire contents of the edit box,
316     * this subclass method filters on the range from
317     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
318     * if the length of that range meets or exceeds {@link #getThreshold}
319     * and makes sure that the range is not already a Chip.
320     */
321    @Override
322    protected void performFiltering(CharSequence text, int keyCode) {
323        if (enoughToFilter()) {
324            int end = getSelectionEnd();
325            int start = mTokenizer.findTokenStart(text, end);
326            // If this is a RecipientChip, don't filter
327            // on its contents.
328            Spannable span = getSpannable();
329            RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
330            if (chips != null && chips.length > 0) {
331                return;
332            }
333        }
334        super.performFiltering(text, keyCode);
335    }
336
337    private void clearSelectedChip() {
338        if (mSelectedChip != null) {
339            mSelectedChip.unselectChip();
340            mSelectedChip = null;
341        }
342        setCursorVisible(true);
343    }
344
345    @Override
346    public boolean onTouchEvent(MotionEvent event) {
347        int action = event.getAction();
348        boolean handled = super.onTouchEvent(event);
349        boolean chipWasSelected = false;
350
351        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
352            float x = event.getX();
353            float y = event.getY();
354            int offset = putOffsetInRange(getOffsetForPosition(x, y));
355            RecipientChip currentChip = findChip(offset);
356            if (currentChip != null) {
357                if (action == MotionEvent.ACTION_UP) {
358                    if (mSelectedChip != null && mSelectedChip != currentChip) {
359                        clearSelectedChip();
360                        mSelectedChip = currentChip.selectChip();
361                        setCursorVisible(false);
362                    } else if (mSelectedChip == null) {
363                        mSelectedChip = currentChip.selectChip();
364                        setCursorVisible(false);
365                    } else {
366                        mSelectedChip.onClick(this, offset, x, y);
367                    }
368                }
369                chipWasSelected = true;
370            }
371        }
372        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
373            clearSelectedChip();
374        }
375        return handled;
376    }
377
378    // TODO: This algorithm will need a lot of tweaking after more people have used
379    // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
380    // what comes before the finger.
381    private int putOffsetInRange(int o) {
382        int offset = o;
383        Editable text = getText();
384        int length = text.length();
385        // Remove whitespace from end to find "real end"
386        int realLength = length;
387        for (int i = length - 1; i >= 0; i--) {
388            if (text.charAt(i) == ' ') {
389                realLength--;
390            } else {
391                break;
392            }
393        }
394
395        // If the offset is beyond or at the end of the text,
396        // leave it alone.
397        if (offset >= realLength) {
398            return offset;
399        }
400        Editable editable = getText();
401        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
402            // Keep walking backward!
403            offset--;
404        }
405        return offset;
406    }
407
408    private int findText(Editable text, int offset) {
409        if (text.charAt(offset) != ' ') {
410            return offset;
411        }
412        return -1;
413    }
414
415    private RecipientChip findChip(int offset) {
416        RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class);
417        // Find the chip that contains this offset.
418        for (int i = 0; i < chips.length; i++) {
419            RecipientChip chip = chips[i];
420            if (chip.matchesChip(offset)) {
421                return chip;
422            }
423        }
424        return null;
425    }
426
427    private CharSequence createChip(RecipientEntry entry) {
428        CharSequence displayText = mTokenizer.terminateToken(entry.getDestination());
429        // Always leave a blank space at the end of a chip.
430        int textLength = displayText.length();
431        if (displayText.charAt(textLength - 1) == ' ') {
432            textLength--;
433        } else {
434            displayText = displayText.toString().concat(" ");
435            textLength = displayText.length();
436        }
437        SpannableString chipText = new SpannableString(displayText);
438        int end = getSelectionEnd();
439        int start = mTokenizer.findTokenStart(getText(), end);
440        try {
441            chipText.setSpan(constructChipSpan(entry, start, false), 0, textLength,
442                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
443        } catch (NullPointerException e) {
444            Log.e(TAG, e.getMessage(), e);
445            return null;
446        }
447
448        return chipText;
449    }
450
451    @Override
452    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
453        submitItemAtPosition(position);
454    }
455
456    private void submitItemAtPosition(int position) {
457        RecipientEntry entry = (RecipientEntry) getAdapter().getItem(position);
458        clearComposingText();
459
460        int end = getSelectionEnd();
461        int start = mTokenizer.findTokenStart(getText(), end);
462
463        Editable editable = getText();
464        editable.replace(start, end, createChip(entry));
465        QwertyKeyListener.markAsReplaced(editable, start, end, "");
466    }
467
468    /** Returns a collection of contact Id for each chip inside this View. */
469    /* package */ Collection<Long> getContactIds() {
470        final Set<Long> result = new HashSet<Long>();
471        for (RecipientChip chip : mRecipients) {
472            result.add(chip.getContactId());
473        }
474        return result;
475    }
476
477    /** Returns a collection of data Id for each chip inside this View. May be null. */
478    /* package */ Collection<Long> getDataIds() {
479        final Set<Long> result = new HashSet<Long>();
480        for (RecipientChip chip : mRecipients) {
481            result.add(chip.getDataId());
482        }
483        return result;
484    }
485
486    /**
487     * RecipientChip defines an ImageSpan that contains information relevant to
488     * a particular recipient.
489     */
490    public class RecipientChip extends ImageSpan implements OnItemClickListener {
491        private final CharSequence mDisplay;
492
493        private final CharSequence mValue;
494
495        private final int mOffset;
496
497        private ListPopupWindow mPopup;
498
499        private View mAnchorView;
500
501        private int mLeft;
502
503        private final long mContactId;
504
505        private final long mDataId;
506
507        private RecipientEntry mEntry;
508
509        private boolean mSelected = false;
510
511        private RecipientAlternatesAdapter mAlternatesAdapter;
512
513        private Rect mBounds;
514
515        public RecipientChip(Drawable drawable, RecipientEntry entry, int offset, Rect bounds) {
516            super(drawable);
517            mDisplay = entry.getDisplayName();
518            mValue = entry.getDestination();
519            mContactId = entry.getContactId();
520            mDataId = entry.getDataId();
521            mOffset = offset;
522            mEntry = entry;
523            mBounds = bounds;
524
525            mAnchorView = new View(getContext());
526            mAnchorView.setLeft(bounds.left);
527            mAnchorView.setRight(bounds.left);
528            mAnchorView.setTop(bounds.bottom);
529            mAnchorView.setBottom(bounds.bottom);
530            mAnchorView.setVisibility(View.GONE);
531            mRecipients.add(this);
532        }
533
534        public void unselectChip() {
535            if (getChipStart() == -1 || getChipEnd() == -1) {
536                mSelectedChip = null;
537                return;
538            }
539            clearComposingText();
540            RecipientChip newChipSpan = null;
541            try {
542                newChipSpan = constructChipSpan(mEntry, mOffset, false);
543            } catch (NullPointerException e) {
544                Log.e(TAG, e.getMessage(), e);
545                return;
546            }
547            replace(newChipSpan);
548            if (mPopup != null && mPopup.isShowing()) {
549                mPopup.dismiss();
550            }
551            return;
552        }
553
554        public void onKeyDown(int keyCode, KeyEvent event) {
555            if (keyCode == KeyEvent.KEYCODE_DEL) {
556                if (mPopup != null && mPopup.isShowing()) {
557                    mPopup.dismiss();
558                }
559                removeChip();
560            }
561        }
562
563        public boolean isCompletedContact() {
564            return mContactId != -1;
565        }
566
567        private void replace(RecipientChip newChip) {
568            Spannable spannable = getSpannable();
569            int spanStart = getChipStart();
570            int spanEnd = getChipEnd();
571            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
572            spannable.removeSpan(this);
573            mRecipients.remove(this);
574            spannable.setSpan(newChip, spanStart, spanEnd, 0);
575        }
576
577        public void removeChip() {
578            Spannable spannable = getSpannable();
579            int spanStart = getChipStart();
580            int spanEnd = getChipEnd();
581            Editable text = getText();
582            int toDelete = spanEnd;
583            // Always remove trailing spaces when removing a chip.
584            while (toDelete >= 0 && toDelete < text.length() - 1 && text.charAt(toDelete) == ' ') {
585                toDelete++;
586            }
587            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
588            spannable.removeSpan(this);
589            mRecipients.remove(this);
590            spannable.setSpan(null, spanStart, spanEnd, 0);
591            text.delete(spanStart, toDelete);
592            if (this == mSelectedChip) {
593                mSelectedChip = null;
594                clearSelectedChip();
595            }
596        }
597
598        public int getChipStart() {
599            return getSpannable().getSpanStart(this);
600        }
601
602        public int getChipEnd() {
603            return getSpannable().getSpanEnd(this);
604        }
605
606        public void replaceChip(RecipientEntry entry) {
607            clearComposingText();
608
609            RecipientChip newChipSpan = null;
610            try {
611                newChipSpan = constructChipSpan(entry, mOffset, false);
612            } catch (NullPointerException e) {
613                Log.e(TAG, e.getMessage(), e);
614                return;
615            }
616            replace(newChipSpan);
617            if (mPopup != null && mPopup.isShowing()) {
618                mPopup.dismiss();
619            }
620        }
621
622        public RecipientChip selectChip() {
623            clearComposingText();
624            RecipientChip newChipSpan = null;
625            if (isCompletedContact()) {
626                try {
627                    newChipSpan = constructChipSpan(mEntry, mOffset, true);
628                    newChipSpan.setSelected(true);
629                } catch (NullPointerException e) {
630                    Log.e(TAG, e.getMessage(), e);
631                    return newChipSpan;
632                }
633                replace(newChipSpan);
634                if (mPopup != null && mPopup.isShowing()) {
635                    mPopup.dismiss();
636                }
637                mSelected = true;
638                // Make sure we call edit on the new chip span.
639                newChipSpan.showAlternates();
640            } else {
641                CharSequence text = getValue();
642                removeChip();
643                Editable editable = getText();
644                setSelection(editable.length());
645                editable.append(text);
646            }
647            return newChipSpan;
648        }
649
650        private void showAlternates() {
651            mPopup = new ListPopupWindow(RecipientEditTextView.this.getContext());
652
653            if (!mPopup.isShowing()) {
654                mAlternatesAdapter = new RecipientAlternatesAdapter(
655                        RecipientEditTextView.this.getContext(),
656                        mEntry.getContactId(), mEntry.getDataId(),
657                        mAlternatesLayout, mAlternatesSelectedLayout);
658                mAnchorView.setLeft(mLeft);
659                mAnchorView.setRight(mLeft);
660                mPopup.setAnchorView(mAnchorView);
661                mPopup.setAdapter(mAlternatesAdapter);
662                mPopup.setWidth(getWidth());
663                mPopup.setOnItemClickListener(this);
664                mPopup.show();
665            }
666        }
667
668        private void setSelected(boolean selected) {
669            mSelected = selected;
670        }
671
672        public CharSequence getDisplay() {
673            return mDisplay;
674        }
675
676        public CharSequence getValue() {
677            return mValue;
678        }
679
680        private boolean isInDelete(int offset, float x, float y) {
681            // Figure out the bounds of this chip and whether or not
682            // the user clicked in the X portion.
683            return mSelected
684                    && (offset == getChipEnd()
685                            || (x > (mBounds.right - mChipDeleteWidth) && x < mBounds.right));
686        }
687
688        public boolean matchesChip(int offset) {
689            int start = getChipStart();
690            int end = getChipEnd();
691            return (offset >= start && offset <= end);
692        }
693
694        public void onClick(View widget, int offset, float x, float y) {
695            if (mSelected) {
696                if (isInDelete(offset, x, y)) {
697                    removeChip();
698                    return;
699                }
700            }
701        }
702
703        @Override
704        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
705                int y, int bottom, Paint paint) {
706            // Shift the bounds of this span to where it is actually drawn on the screeen.
707            mLeft = (int) x;
708            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
709        }
710
711        @Override
712        public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) {
713            mPopup.dismiss();
714            clearComposingText();
715            replaceChip(mAlternatesAdapter.getRecipientEntry(position));
716        }
717
718        public long getContactId() {
719            return mContactId;
720        }
721
722        public long getDataId() {
723            return mDataId;
724        }
725    }
726
727    @Override
728    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
729        return false;
730    }
731
732    @Override
733    public void onDestroyActionMode(ActionMode mode) {
734    }
735
736    @Override
737    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
738        return false;
739    }
740
741    // Prevent selection of chips.
742    @Override
743    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
744        return false;
745    }
746}
747
748