RecipientEditTextView.java revision 55bb2833b29945c08b809408ff94ddf7703e911a
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 } 343 344 @Override 345 public boolean onTouchEvent(MotionEvent event) { 346 int action = event.getAction(); 347 boolean handled = super.onTouchEvent(event); 348 boolean chipWasSelected = false; 349 350 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 351 float x = event.getX(); 352 float y = event.getY(); 353 int offset = putOffsetInRange(getOffsetForPosition(x, y)); 354 RecipientChip currentChip = findChip(offset); 355 if (currentChip != null) { 356 if (action == MotionEvent.ACTION_UP) { 357 if (mSelectedChip != null && mSelectedChip != currentChip) { 358 clearSelectedChip(); 359 mSelectedChip = currentChip.selectChip(); 360 } else if (mSelectedChip == null) { 361 mSelectedChip = currentChip.selectChip(); 362 } else { 363 mSelectedChip.onClick(this, offset, x, y); 364 } 365 } 366 chipWasSelected = true; 367 } 368 } 369 if (action == MotionEvent.ACTION_UP && !chipWasSelected) { 370 clearSelectedChip(); 371 } 372 return handled; 373 } 374 375 // TODO: This algorithm will need a lot of tweaking after more people have used 376 // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring 377 // what comes before the finger. 378 private int putOffsetInRange(int o) { 379 int offset = o; 380 Editable text = getText(); 381 int length = text.length(); 382 // Remove whitespace from end to find "real end" 383 int realLength = length; 384 for (int i = length - 1; i >= 0; i--) { 385 if (text.charAt(i) == ' ') { 386 realLength--; 387 } else { 388 break; 389 } 390 } 391 392 // If the offset is beyond or at the end of the text, 393 // leave it alone. 394 if (offset >= realLength) { 395 return offset; 396 } 397 Editable editable = getText(); 398 while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) { 399 // Keep walking backward! 400 offset--; 401 } 402 return offset; 403 } 404 405 private int findText(Editable text, int offset) { 406 if (text.charAt(offset) != ' ') { 407 return offset; 408 } 409 return -1; 410 } 411 412 private RecipientChip findChip(int offset) { 413 RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class); 414 // Find the chip that contains this offset. 415 for (int i = 0; i < chips.length; i++) { 416 RecipientChip chip = chips[i]; 417 if (chip.matchesChip(offset)) { 418 return chip; 419 } 420 } 421 return null; 422 } 423 424 private CharSequence createChip(RecipientEntry entry) { 425 CharSequence displayText = mTokenizer.terminateToken(entry.getDestination()); 426 // Always leave a blank space at the end of a chip. 427 int textLength = displayText.length(); 428 if (displayText.charAt(textLength - 1) == ' ') { 429 textLength--; 430 } else { 431 displayText = displayText.toString().concat(" "); 432 textLength = displayText.length(); 433 } 434 SpannableString chipText = new SpannableString(displayText); 435 int end = getSelectionEnd(); 436 int start = mTokenizer.findTokenStart(getText(), end); 437 try { 438 chipText.setSpan(constructChipSpan(entry, start, false), 0, textLength, 439 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 440 } catch (NullPointerException e) { 441 Log.e(TAG, e.getMessage(), e); 442 return null; 443 } 444 445 return chipText; 446 } 447 448 @Override 449 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 450 submitItemAtPosition(position); 451 } 452 453 private void submitItemAtPosition(int position) { 454 RecipientEntry entry = (RecipientEntry) getAdapter().getItem(position); 455 clearComposingText(); 456 457 int end = getSelectionEnd(); 458 int start = mTokenizer.findTokenStart(getText(), end); 459 460 Editable editable = getText(); 461 editable.replace(start, end, createChip(entry)); 462 QwertyKeyListener.markAsReplaced(editable, start, end, ""); 463 } 464 465 /** Returns a collection of contact Id for each chip inside this View. */ 466 /* package */ Collection<Long> getContactIds() { 467 final Set<Long> result = new HashSet<Long>(); 468 for (RecipientChip chip : mRecipients) { 469 result.add(chip.getContactId()); 470 } 471 return result; 472 } 473 474 /** Returns a collection of data Id for each chip inside this View. May be null. */ 475 /* package */ Collection<Long> getDataIds() { 476 final Set<Long> result = new HashSet<Long>(); 477 for (RecipientChip chip : mRecipients) { 478 result.add(chip.getDataId()); 479 } 480 return result; 481 } 482 483 /** 484 * RecipientChip defines an ImageSpan that contains information relevant to 485 * a particular recipient. 486 */ 487 public class RecipientChip extends ImageSpan implements OnItemClickListener { 488 private final CharSequence mDisplay; 489 490 private final CharSequence mValue; 491 492 private final int mOffset; 493 494 private ListPopupWindow mPopup; 495 496 private View mAnchorView; 497 498 private int mLeft; 499 500 private final long mContactId; 501 502 private final long mDataId; 503 504 private RecipientEntry mEntry; 505 506 private boolean mSelected = false; 507 508 private RecipientAlternatesAdapter mAlternatesAdapter; 509 510 private Rect mBounds; 511 512 public RecipientChip(Drawable drawable, RecipientEntry entry, int offset, Rect bounds) { 513 super(drawable); 514 mDisplay = entry.getDisplayName(); 515 mValue = entry.getDestination(); 516 mContactId = entry.getContactId(); 517 mDataId = entry.getDataId(); 518 mOffset = offset; 519 mEntry = entry; 520 mBounds = bounds; 521 522 mAnchorView = new View(getContext()); 523 mAnchorView.setLeft(bounds.left); 524 mAnchorView.setRight(bounds.left); 525 mAnchorView.setTop(bounds.bottom); 526 mAnchorView.setBottom(bounds.bottom); 527 mAnchorView.setVisibility(View.GONE); 528 mRecipients.add(this); 529 } 530 531 public void unselectChip() { 532 if (getChipStart() == -1 || getChipEnd() == -1) { 533 mSelectedChip = null; 534 return; 535 } 536 clearComposingText(); 537 RecipientChip newChipSpan = null; 538 try { 539 newChipSpan = constructChipSpan(mEntry, mOffset, false); 540 } catch (NullPointerException e) { 541 Log.e(TAG, e.getMessage(), e); 542 return; 543 } 544 replace(newChipSpan); 545 if (mPopup != null && mPopup.isShowing()) { 546 mPopup.dismiss(); 547 } 548 return; 549 } 550 551 public void onKeyDown(int keyCode, KeyEvent event) { 552 if (keyCode == KeyEvent.KEYCODE_DEL) { 553 if (mPopup != null && mPopup.isShowing()) { 554 mPopup.dismiss(); 555 } 556 removeChip(); 557 } 558 } 559 560 public boolean isCompletedContact() { 561 return mContactId != -1; 562 } 563 564 private void replace(RecipientChip newChip) { 565 Spannable spannable = getSpannable(); 566 int spanStart = getChipStart(); 567 int spanEnd = getChipEnd(); 568 QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, ""); 569 spannable.removeSpan(this); 570 mRecipients.remove(this); 571 spannable.setSpan(newChip, spanStart, spanEnd, 0); 572 } 573 574 public void removeChip() { 575 Spannable spannable = getSpannable(); 576 int spanStart = getChipStart(); 577 int spanEnd = getChipEnd(); 578 if (this == mSelectedChip) { 579 mSelectedChip = null; 580 } 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 } 593 594 public int getChipStart() { 595 return getSpannable().getSpanStart(this); 596 } 597 598 public int getChipEnd() { 599 return getSpannable().getSpanEnd(this); 600 } 601 602 public void replaceChip(RecipientEntry entry) { 603 clearComposingText(); 604 605 RecipientChip newChipSpan = null; 606 try { 607 newChipSpan = constructChipSpan(entry, mOffset, false); 608 } catch (NullPointerException e) { 609 Log.e(TAG, e.getMessage(), e); 610 return; 611 } 612 replace(newChipSpan); 613 if (mPopup != null && mPopup.isShowing()) { 614 mPopup.dismiss(); 615 } 616 } 617 618 public RecipientChip selectChip() { 619 clearComposingText(); 620 RecipientChip newChipSpan = null; 621 if (isCompletedContact()) { 622 try { 623 newChipSpan = constructChipSpan(mEntry, mOffset, true); 624 newChipSpan.setSelected(true); 625 } catch (NullPointerException e) { 626 Log.e(TAG, e.getMessage(), e); 627 return newChipSpan; 628 } 629 replace(newChipSpan); 630 if (mPopup != null && mPopup.isShowing()) { 631 mPopup.dismiss(); 632 } 633 mSelected = true; 634 // Make sure we call edit on the new chip span. 635 newChipSpan.showAlternates(); 636 } else { 637 CharSequence text = getValue(); 638 removeChip(); 639 Editable editable = getText(); 640 setSelection(editable.length()); 641 editable.append(text); 642 } 643 return newChipSpan; 644 } 645 646 private void showAlternates() { 647 mPopup = new ListPopupWindow(RecipientEditTextView.this.getContext()); 648 649 if (!mPopup.isShowing()) { 650 mAlternatesAdapter = new RecipientAlternatesAdapter( 651 RecipientEditTextView.this.getContext(), 652 mEntry.getContactId(), mEntry.getDataId(), 653 mAlternatesLayout, mAlternatesSelectedLayout); 654 mAnchorView.setLeft(mLeft); 655 mAnchorView.setRight(mLeft); 656 mPopup.setAnchorView(mAnchorView); 657 mPopup.setAdapter(mAlternatesAdapter); 658 mPopup.setWidth(getWidth()); 659 mPopup.setOnItemClickListener(this); 660 mPopup.show(); 661 } 662 } 663 664 private void setSelected(boolean selected) { 665 mSelected = selected; 666 } 667 668 public CharSequence getDisplay() { 669 return mDisplay; 670 } 671 672 public CharSequence getValue() { 673 return mValue; 674 } 675 676 private boolean isInDelete(int offset, float x, float y) { 677 // Figure out the bounds of this chip and whether or not 678 // the user clicked in the X portion. 679 return mSelected 680 && (offset == getChipEnd() 681 || (x > (mBounds.right - mChipDeleteWidth) && x < mBounds.right)); 682 } 683 684 public boolean matchesChip(int offset) { 685 int start = getChipStart(); 686 int end = getChipEnd(); 687 return (offset >= start && offset <= end); 688 } 689 690 public void onClick(View widget, int offset, float x, float y) { 691 if (mSelected) { 692 if (isInDelete(offset, x, y)) { 693 removeChip(); 694 return; 695 } 696 } 697 } 698 699 @Override 700 public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, 701 int y, int bottom, Paint paint) { 702 // Shift the bounds of this span to where it is actually drawn on the screeen. 703 mLeft = (int) x; 704 super.draw(canvas, text, start, end, x, top, y, bottom, paint); 705 } 706 707 @Override 708 public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) { 709 mPopup.dismiss(); 710 clearComposingText(); 711 replaceChip(mAlternatesAdapter.getRecipientEntry(position)); 712 } 713 714 public long getContactId() { 715 return mContactId; 716 } 717 718 public long getDataId() { 719 return mDataId; 720 } 721 } 722 723 @Override 724 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 725 return false; 726 } 727 728 @Override 729 public void onDestroyActionMode(ActionMode mode) { 730 } 731 732 @Override 733 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 734 return false; 735 } 736 737 // Prevent selection of chips. 738 @Override 739 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 740 return false; 741 } 742} 743 744