1 /* 2 * Copyright (C) 2006 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 17 package com.android.internal.view.menu; 18 19 20 import android.annotation.NonNull; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.content.res.Configuration; 28 import android.content.res.Resources; 29 import android.graphics.drawable.Drawable; 30 import android.os.Bundle; 31 import android.os.Parcelable; 32 import android.util.SparseArray; 33 import android.view.ActionProvider; 34 import android.view.ContextMenu.ContextMenuInfo; 35 import android.view.KeyCharacterMap; 36 import android.view.KeyEvent; 37 import android.view.Menu; 38 import android.view.MenuItem; 39 import android.view.SubMenu; 40 import android.view.View; 41 import android.view.ViewConfiguration; 42 43 import java.lang.ref.WeakReference; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.concurrent.CopyOnWriteArrayList; 47 48 /** 49 * Implementation of the {@link android.view.Menu} interface for creating a 50 * standard menu UI. 51 */ 52 public class MenuBuilder implements Menu { 53 private static final String TAG = "MenuBuilder"; 54 55 private static final String PRESENTER_KEY = "android:menu:presenters"; 56 private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates"; 57 private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview"; 58 59 private static final int[] sCategoryToOrder = new int[] { 60 1, /* No category */ 61 4, /* CONTAINER */ 62 5, /* SYSTEM */ 63 3, /* SECONDARY */ 64 2, /* ALTERNATIVE */ 65 0, /* SELECTED_ALTERNATIVE */ 66 }; 67 68 @UnsupportedAppUsage 69 private final Context mContext; 70 private final Resources mResources; 71 72 /** 73 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode() 74 * instead of accessing this directly. 75 */ 76 private boolean mQwertyMode; 77 78 /** 79 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible() 80 * instead of accessing this directly. 81 */ 82 private boolean mShortcutsVisible; 83 84 /** 85 * Callback that will receive the various menu-related events generated by 86 * this class. Use getCallback to get a reference to the callback. 87 */ 88 private Callback mCallback; 89 90 /** Contains all of the items for this menu */ 91 private ArrayList<MenuItemImpl> mItems; 92 93 /** Contains only the items that are currently visible. This will be created/refreshed from 94 * {@link #getVisibleItems()} */ 95 private ArrayList<MenuItemImpl> mVisibleItems; 96 /** 97 * Whether or not the items (or any one item's shown state) has changed since it was last 98 * fetched from {@link #getVisibleItems()} 99 */ 100 private boolean mIsVisibleItemsStale; 101 102 /** 103 * Contains only the items that should appear in the Action Bar, if present. 104 */ 105 private ArrayList<MenuItemImpl> mActionItems; 106 /** 107 * Contains items that should NOT appear in the Action Bar, if present. 108 */ 109 private ArrayList<MenuItemImpl> mNonActionItems; 110 111 /** 112 * Whether or not the items (or any one item's action state) has changed since it was 113 * last fetched. 114 */ 115 private boolean mIsActionItemsStale; 116 117 /** 118 * Default value for how added items should show in the action list. 119 */ 120 private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER; 121 122 /** 123 * Current use case is Context Menus: As Views populate the context menu, each one has 124 * extra information that should be passed along. This is the current menu info that 125 * should be set on all items added to this menu. 126 */ 127 private ContextMenuInfo mCurrentMenuInfo; 128 129 /** Header title for menu types that have a header (context and submenus) */ 130 CharSequence mHeaderTitle; 131 /** Header icon for menu types that have a header and support icons (context) */ 132 Drawable mHeaderIcon; 133 /** Header custom view for menu types that have a header and support custom views (context) */ 134 View mHeaderView; 135 136 /** 137 * Contains the state of the View hierarchy for all menu views when the menu 138 * was frozen. 139 */ 140 private SparseArray<Parcelable> mFrozenViewStates; 141 142 /** 143 * Prevents onItemsChanged from doing its junk, useful for batching commands 144 * that may individually call onItemsChanged. 145 */ 146 private boolean mPreventDispatchingItemsChanged = false; 147 private boolean mItemsChangedWhileDispatchPrevented = false; 148 149 private boolean mOptionalIconsVisible = false; 150 151 private boolean mIsClosing = false; 152 153 private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>(); 154 155 private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters = 156 new CopyOnWriteArrayList<WeakReference<MenuPresenter>>(); 157 158 /** 159 * Currently expanded menu item; must be collapsed when we clear. 160 */ 161 private MenuItemImpl mExpandedItem; 162 163 /** 164 * Whether group dividers are enabled. 165 */ 166 private boolean mGroupDividerEnabled = false; 167 168 /** 169 * Called by menu to notify of close and selection changes. 170 */ 171 public interface Callback { 172 /** 173 * Called when a menu item is selected. 174 * @param menu The menu that is the parent of the item 175 * @param item The menu item that is selected 176 * @return whether the menu item selection was handled 177 */ 178 @UnsupportedAppUsage onMenuItemSelected(MenuBuilder menu, MenuItem item)179 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item); 180 181 /** 182 * Called when the mode of the menu changes (for example, from icon to expanded). 183 * 184 * @param menu the menu that has changed modes 185 */ 186 @UnsupportedAppUsage onMenuModeChange(MenuBuilder menu)187 public void onMenuModeChange(MenuBuilder menu); 188 } 189 190 /** 191 * Called by menu items to execute their associated action 192 */ 193 public interface ItemInvoker { invokeItem(MenuItemImpl item)194 public boolean invokeItem(MenuItemImpl item); 195 } 196 197 @UnsupportedAppUsage MenuBuilder(Context context)198 public MenuBuilder(Context context) { 199 mContext = context; 200 mResources = context.getResources(); 201 mItems = new ArrayList<MenuItemImpl>(); 202 203 mVisibleItems = new ArrayList<MenuItemImpl>(); 204 mIsVisibleItemsStale = true; 205 206 mActionItems = new ArrayList<MenuItemImpl>(); 207 mNonActionItems = new ArrayList<MenuItemImpl>(); 208 mIsActionItemsStale = true; 209 210 setShortcutsVisibleInner(true); 211 } 212 213 @UnsupportedAppUsage setDefaultShowAsAction(int defaultShowAsAction)214 public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) { 215 mDefaultShowAsAction = defaultShowAsAction; 216 return this; 217 } 218 219 /** 220 * Add a presenter to this menu. This will only hold a WeakReference; 221 * you do not need to explicitly remove a presenter, but you can using 222 * {@link #removeMenuPresenter(MenuPresenter)}. 223 * 224 * @param presenter The presenter to add 225 */ 226 @UnsupportedAppUsage addMenuPresenter(MenuPresenter presenter)227 public void addMenuPresenter(MenuPresenter presenter) { 228 addMenuPresenter(presenter, mContext); 229 } 230 231 /** 232 * Add a presenter to this menu that uses an alternate context for 233 * inflating menu items. This will only hold a WeakReference; you do not 234 * need to explicitly remove a presenter, but you can using 235 * {@link #removeMenuPresenter(MenuPresenter)}. 236 * 237 * @param presenter The presenter to add 238 * @param menuContext The context used to inflate menu items 239 */ 240 @UnsupportedAppUsage addMenuPresenter(MenuPresenter presenter, Context menuContext)241 public void addMenuPresenter(MenuPresenter presenter, Context menuContext) { 242 mPresenters.add(new WeakReference<MenuPresenter>(presenter)); 243 presenter.initForMenu(menuContext, this); 244 mIsActionItemsStale = true; 245 } 246 247 /** 248 * Remove a presenter from this menu. That presenter will no longer 249 * receive notifications of updates to this menu's data. 250 * 251 * @param presenter The presenter to remove 252 */ 253 @UnsupportedAppUsage removeMenuPresenter(MenuPresenter presenter)254 public void removeMenuPresenter(MenuPresenter presenter) { 255 for (WeakReference<MenuPresenter> ref : mPresenters) { 256 final MenuPresenter item = ref.get(); 257 if (item == null || item == presenter) { 258 mPresenters.remove(ref); 259 } 260 } 261 } 262 dispatchPresenterUpdate(boolean cleared)263 private void dispatchPresenterUpdate(boolean cleared) { 264 if (mPresenters.isEmpty()) return; 265 266 stopDispatchingItemsChanged(); 267 for (WeakReference<MenuPresenter> ref : mPresenters) { 268 final MenuPresenter presenter = ref.get(); 269 if (presenter == null) { 270 mPresenters.remove(ref); 271 } else { 272 presenter.updateMenuView(cleared); 273 } 274 } 275 startDispatchingItemsChanged(); 276 } 277 dispatchSubMenuSelected(SubMenuBuilder subMenu, MenuPresenter preferredPresenter)278 private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu, 279 MenuPresenter preferredPresenter) { 280 if (mPresenters.isEmpty()) return false; 281 282 boolean result = false; 283 284 // Try the preferred presenter first. 285 if (preferredPresenter != null) { 286 result = preferredPresenter.onSubMenuSelected(subMenu); 287 } 288 289 for (WeakReference<MenuPresenter> ref : mPresenters) { 290 final MenuPresenter presenter = ref.get(); 291 if (presenter == null) { 292 mPresenters.remove(ref); 293 } else if (!result) { 294 result = presenter.onSubMenuSelected(subMenu); 295 } 296 } 297 return result; 298 } 299 dispatchSaveInstanceState(Bundle outState)300 private void dispatchSaveInstanceState(Bundle outState) { 301 if (mPresenters.isEmpty()) return; 302 303 SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>(); 304 305 for (WeakReference<MenuPresenter> ref : mPresenters) { 306 final MenuPresenter presenter = ref.get(); 307 if (presenter == null) { 308 mPresenters.remove(ref); 309 } else { 310 final int id = presenter.getId(); 311 if (id > 0) { 312 final Parcelable state = presenter.onSaveInstanceState(); 313 if (state != null) { 314 presenterStates.put(id, state); 315 } 316 } 317 } 318 } 319 320 outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates); 321 } 322 dispatchRestoreInstanceState(Bundle state)323 private void dispatchRestoreInstanceState(Bundle state) { 324 SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY); 325 326 if (presenterStates == null || mPresenters.isEmpty()) return; 327 328 for (WeakReference<MenuPresenter> ref : mPresenters) { 329 final MenuPresenter presenter = ref.get(); 330 if (presenter == null) { 331 mPresenters.remove(ref); 332 } else { 333 final int id = presenter.getId(); 334 if (id > 0) { 335 Parcelable parcel = presenterStates.get(id); 336 if (parcel != null) { 337 presenter.onRestoreInstanceState(parcel); 338 } 339 } 340 } 341 } 342 } 343 savePresenterStates(Bundle outState)344 public void savePresenterStates(Bundle outState) { 345 dispatchSaveInstanceState(outState); 346 } 347 restorePresenterStates(Bundle state)348 public void restorePresenterStates(Bundle state) { 349 dispatchRestoreInstanceState(state); 350 } 351 saveActionViewStates(Bundle outStates)352 public void saveActionViewStates(Bundle outStates) { 353 SparseArray<Parcelable> viewStates = null; 354 355 final int itemCount = size(); 356 for (int i = 0; i < itemCount; i++) { 357 final MenuItem item = getItem(i); 358 final View v = item.getActionView(); 359 if (v != null && v.getId() != View.NO_ID) { 360 if (viewStates == null) { 361 viewStates = new SparseArray<Parcelable>(); 362 } 363 v.saveHierarchyState(viewStates); 364 if (item.isActionViewExpanded()) { 365 outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId()); 366 } 367 } 368 if (item.hasSubMenu()) { 369 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); 370 subMenu.saveActionViewStates(outStates); 371 } 372 } 373 374 if (viewStates != null) { 375 outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates); 376 } 377 } 378 restoreActionViewStates(Bundle states)379 public void restoreActionViewStates(Bundle states) { 380 if (states == null) { 381 return; 382 } 383 384 SparseArray<Parcelable> viewStates = states.getSparseParcelableArray( 385 getActionViewStatesKey()); 386 387 final int itemCount = size(); 388 for (int i = 0; i < itemCount; i++) { 389 final MenuItem item = getItem(i); 390 final View v = item.getActionView(); 391 if (v != null && v.getId() != View.NO_ID) { 392 v.restoreHierarchyState(viewStates); 393 } 394 if (item.hasSubMenu()) { 395 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); 396 subMenu.restoreActionViewStates(states); 397 } 398 } 399 400 final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID); 401 if (expandedId > 0) { 402 MenuItem itemToExpand = findItem(expandedId); 403 if (itemToExpand != null) { 404 itemToExpand.expandActionView(); 405 } 406 } 407 } 408 getActionViewStatesKey()409 protected String getActionViewStatesKey() { 410 return ACTION_VIEW_STATES_KEY; 411 } 412 413 @UnsupportedAppUsage setCallback(Callback cb)414 public void setCallback(Callback cb) { 415 mCallback = cb; 416 } 417 418 /** 419 * Adds an item to the menu. The other add methods funnel to this. 420 */ addInternal(int group, int id, int categoryOrder, CharSequence title)421 private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) { 422 final int ordering = getOrdering(categoryOrder); 423 424 final MenuItemImpl item = createNewMenuItem(group, id, categoryOrder, ordering, title, 425 mDefaultShowAsAction); 426 427 if (mCurrentMenuInfo != null) { 428 // Pass along the current menu info 429 item.setMenuInfo(mCurrentMenuInfo); 430 } 431 432 mItems.add(findInsertIndex(mItems, ordering), item); 433 onItemsChanged(true); 434 435 return item; 436 } 437 438 // Layoutlib overrides this method to return its custom implementation of MenuItemImpl createNewMenuItem(int group, int id, int categoryOrder, int ordering, CharSequence title, int defaultShowAsAction)439 private MenuItemImpl createNewMenuItem(int group, int id, int categoryOrder, int ordering, 440 CharSequence title, int defaultShowAsAction) { 441 return new MenuItemImpl(this, group, id, categoryOrder, ordering, title, 442 defaultShowAsAction); 443 } 444 add(CharSequence title)445 public MenuItem add(CharSequence title) { 446 return addInternal(0, 0, 0, title); 447 } 448 add(int titleRes)449 public MenuItem add(int titleRes) { 450 return addInternal(0, 0, 0, mResources.getString(titleRes)); 451 } 452 add(int group, int id, int categoryOrder, CharSequence title)453 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) { 454 return addInternal(group, id, categoryOrder, title); 455 } 456 add(int group, int id, int categoryOrder, int title)457 public MenuItem add(int group, int id, int categoryOrder, int title) { 458 return addInternal(group, id, categoryOrder, mResources.getString(title)); 459 } 460 addSubMenu(CharSequence title)461 public SubMenu addSubMenu(CharSequence title) { 462 return addSubMenu(0, 0, 0, title); 463 } 464 addSubMenu(int titleRes)465 public SubMenu addSubMenu(int titleRes) { 466 return addSubMenu(0, 0, 0, mResources.getString(titleRes)); 467 } 468 addSubMenu(int group, int id, int categoryOrder, CharSequence title)469 public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) { 470 final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title); 471 final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item); 472 item.setSubMenu(subMenu); 473 474 return subMenu; 475 } 476 addSubMenu(int group, int id, int categoryOrder, int title)477 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) { 478 return addSubMenu(group, id, categoryOrder, mResources.getString(title)); 479 } 480 481 @Override setGroupDividerEnabled(boolean groupDividerEnabled)482 public void setGroupDividerEnabled(boolean groupDividerEnabled) { 483 mGroupDividerEnabled = groupDividerEnabled; 484 } 485 isGroupDividerEnabled()486 public boolean isGroupDividerEnabled() { 487 return mGroupDividerEnabled; 488 } 489 addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems)490 public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, 491 Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { 492 PackageManager pm = mContext.getPackageManager(); 493 final List<ResolveInfo> lri = 494 pm.queryIntentActivityOptions(caller, specifics, intent, 0); 495 final int N = lri != null ? lri.size() : 0; 496 497 if ((flags & FLAG_APPEND_TO_GROUP) == 0) { 498 removeGroup(group); 499 } 500 501 for (int i=0; i<N; i++) { 502 final ResolveInfo ri = lri.get(i); 503 Intent rintent = new Intent( 504 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); 505 rintent.setComponent(new ComponentName( 506 ri.activityInfo.applicationInfo.packageName, 507 ri.activityInfo.name)); 508 final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)) 509 .setIcon(ri.loadIcon(pm)) 510 .setIntent(rintent); 511 if (outSpecificItems != null && ri.specificIndex >= 0) { 512 outSpecificItems[ri.specificIndex] = item; 513 } 514 } 515 516 return N; 517 } 518 removeItem(int id)519 public void removeItem(int id) { 520 removeItemAtInt(findItemIndex(id), true); 521 } 522 removeGroup(int group)523 public void removeGroup(int group) { 524 final int i = findGroupIndex(group); 525 526 if (i >= 0) { 527 final int maxRemovable = mItems.size() - i; 528 int numRemoved = 0; 529 while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) { 530 // Don't force update for each one, this method will do it at the end 531 removeItemAtInt(i, false); 532 } 533 534 // Notify menu views 535 onItemsChanged(true); 536 } 537 } 538 539 /** 540 * Remove the item at the given index and optionally forces menu views to 541 * update. 542 * 543 * @param index The index of the item to be removed. If this index is 544 * invalid an exception is thrown. 545 * @param updateChildrenOnMenuViews Whether to force update on menu views. 546 * Please make sure you eventually call this after your batch of 547 * removals. 548 */ removeItemAtInt(int index, boolean updateChildrenOnMenuViews)549 private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) { 550 if ((index < 0) || (index >= mItems.size())) return; 551 552 mItems.remove(index); 553 554 if (updateChildrenOnMenuViews) onItemsChanged(true); 555 } 556 removeItemAt(int index)557 public void removeItemAt(int index) { 558 removeItemAtInt(index, true); 559 } 560 clearAll()561 public void clearAll() { 562 mPreventDispatchingItemsChanged = true; 563 clear(); 564 clearHeader(); 565 mPresenters.clear(); 566 mPreventDispatchingItemsChanged = false; 567 mItemsChangedWhileDispatchPrevented = false; 568 onItemsChanged(true); 569 } 570 clear()571 public void clear() { 572 if (mExpandedItem != null) { 573 collapseItemActionView(mExpandedItem); 574 } 575 mItems.clear(); 576 577 onItemsChanged(true); 578 } 579 setExclusiveItemChecked(MenuItem item)580 void setExclusiveItemChecked(MenuItem item) { 581 final int group = item.getGroupId(); 582 583 final int N = mItems.size(); 584 for (int i = 0; i < N; i++) { 585 MenuItemImpl curItem = mItems.get(i); 586 if (curItem.getGroupId() == group) { 587 if (!curItem.isExclusiveCheckable()) continue; 588 if (!curItem.isCheckable()) continue; 589 590 // Check the item meant to be checked, uncheck the others (that are in the group) 591 curItem.setCheckedInt(curItem == item); 592 } 593 } 594 } 595 setGroupCheckable(int group, boolean checkable, boolean exclusive)596 public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { 597 final int N = mItems.size(); 598 599 for (int i = 0; i < N; i++) { 600 MenuItemImpl item = mItems.get(i); 601 if (item.getGroupId() == group) { 602 item.setExclusiveCheckable(exclusive); 603 item.setCheckable(checkable); 604 } 605 } 606 } 607 setGroupVisible(int group, boolean visible)608 public void setGroupVisible(int group, boolean visible) { 609 final int N = mItems.size(); 610 611 // We handle the notification of items being changed ourselves, so we use setVisibleInt rather 612 // than setVisible and at the end notify of items being changed 613 614 boolean changedAtLeastOneItem = false; 615 for (int i = 0; i < N; i++) { 616 MenuItemImpl item = mItems.get(i); 617 if (item.getGroupId() == group) { 618 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true; 619 } 620 } 621 622 if (changedAtLeastOneItem) onItemsChanged(true); 623 } 624 setGroupEnabled(int group, boolean enabled)625 public void setGroupEnabled(int group, boolean enabled) { 626 final int N = mItems.size(); 627 628 for (int i = 0; i < N; i++) { 629 MenuItemImpl item = mItems.get(i); 630 if (item.getGroupId() == group) { 631 item.setEnabled(enabled); 632 } 633 } 634 } 635 hasVisibleItems()636 public boolean hasVisibleItems() { 637 final int size = size(); 638 639 for (int i = 0; i < size; i++) { 640 MenuItemImpl item = mItems.get(i); 641 if (item.isVisible()) { 642 return true; 643 } 644 } 645 646 return false; 647 } 648 findItem(int id)649 public MenuItem findItem(int id) { 650 final int size = size(); 651 for (int i = 0; i < size; i++) { 652 MenuItemImpl item = mItems.get(i); 653 if (item.getItemId() == id) { 654 return item; 655 } else if (item.hasSubMenu()) { 656 MenuItem possibleItem = item.getSubMenu().findItem(id); 657 658 if (possibleItem != null) { 659 return possibleItem; 660 } 661 } 662 } 663 664 return null; 665 } 666 findItemIndex(int id)667 public int findItemIndex(int id) { 668 final int size = size(); 669 670 for (int i = 0; i < size; i++) { 671 MenuItemImpl item = mItems.get(i); 672 if (item.getItemId() == id) { 673 return i; 674 } 675 } 676 677 return -1; 678 } 679 findGroupIndex(int group)680 public int findGroupIndex(int group) { 681 return findGroupIndex(group, 0); 682 } 683 findGroupIndex(int group, int start)684 public int findGroupIndex(int group, int start) { 685 final int size = size(); 686 687 if (start < 0) { 688 start = 0; 689 } 690 691 for (int i = start; i < size; i++) { 692 final MenuItemImpl item = mItems.get(i); 693 694 if (item.getGroupId() == group) { 695 return i; 696 } 697 } 698 699 return -1; 700 } 701 size()702 public int size() { 703 return mItems.size(); 704 } 705 706 /** {@inheritDoc} */ getItem(int index)707 public MenuItem getItem(int index) { 708 return mItems.get(index); 709 } 710 isShortcutKey(int keyCode, KeyEvent event)711 public boolean isShortcutKey(int keyCode, KeyEvent event) { 712 return findItemWithShortcutForKey(keyCode, event) != null; 713 } 714 setQwertyMode(boolean isQwerty)715 public void setQwertyMode(boolean isQwerty) { 716 mQwertyMode = isQwerty; 717 718 onItemsChanged(false); 719 } 720 721 /** 722 * Returns the ordering across all items. This will grab the category from 723 * the upper bits, find out how to order the category with respect to other 724 * categories, and combine it with the lower bits. 725 * 726 * @param categoryOrder The category order for a particular item (if it has 727 * not been or/add with a category, the default category is 728 * assumed). 729 * @return An ordering integer that can be used to order this item across 730 * all the items (even from other categories). 731 */ getOrdering(int categoryOrder)732 private static int getOrdering(int categoryOrder) { 733 final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; 734 735 if (index < 0 || index >= sCategoryToOrder.length) { 736 throw new IllegalArgumentException("order does not contain a valid category."); 737 } 738 739 return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK); 740 } 741 742 /** 743 * @return whether the menu shortcuts are in qwerty mode or not 744 */ isQwertyMode()745 boolean isQwertyMode() { 746 return mQwertyMode; 747 } 748 749 /** 750 * Sets whether the shortcuts should be visible on menus. Devices without hardware 751 * key input will never make shortcuts visible even if this method is passed 'true'. 752 * 753 * @param shortcutsVisible Whether shortcuts should be visible (if true and a 754 * menu item does not have a shortcut defined, that item will 755 * still NOT show a shortcut) 756 */ setShortcutsVisible(boolean shortcutsVisible)757 public void setShortcutsVisible(boolean shortcutsVisible) { 758 if (mShortcutsVisible == shortcutsVisible) return; 759 760 setShortcutsVisibleInner(shortcutsVisible); 761 onItemsChanged(false); 762 } 763 setShortcutsVisibleInner(boolean shortcutsVisible)764 private void setShortcutsVisibleInner(boolean shortcutsVisible) { 765 mShortcutsVisible = shortcutsVisible 766 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS 767 && ViewConfiguration.get(mContext).shouldShowMenuShortcutsWhenKeyboardPresent(); 768 } 769 770 /** 771 * @return Whether shortcuts should be visible on menus. 772 */ isShortcutsVisible()773 public boolean isShortcutsVisible() { 774 return mShortcutsVisible; 775 } 776 getResources()777 Resources getResources() { 778 return mResources; 779 } 780 781 @UnsupportedAppUsage getContext()782 public Context getContext() { 783 return mContext; 784 } 785 dispatchMenuItemSelected(MenuBuilder menu, MenuItem item)786 boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) { 787 return mCallback != null && mCallback.onMenuItemSelected(menu, item); 788 } 789 790 /** 791 * Dispatch a mode change event to this menu's callback. 792 */ changeMenuMode()793 public void changeMenuMode() { 794 if (mCallback != null) { 795 mCallback.onMenuModeChange(this); 796 } 797 } 798 findInsertIndex(ArrayList<MenuItemImpl> items, int ordering)799 private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { 800 for (int i = items.size() - 1; i >= 0; i--) { 801 MenuItemImpl item = items.get(i); 802 if (item.getOrdering() <= ordering) { 803 return i + 1; 804 } 805 } 806 807 return 0; 808 } 809 performShortcut(int keyCode, KeyEvent event, int flags)810 public boolean performShortcut(int keyCode, KeyEvent event, int flags) { 811 final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event); 812 813 boolean handled = false; 814 815 if (item != null) { 816 handled = performItemAction(item, flags); 817 } 818 819 if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) { 820 close(true /* closeAllMenus */); 821 } 822 823 return handled; 824 } 825 826 /* 827 * This function will return all the menu and sub-menu items that can 828 * be directly (the shortcut directly corresponds) and indirectly 829 * (the ALT-enabled char corresponds to the shortcut) associated 830 * with the keyCode. 831 */ findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event)832 void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) { 833 final boolean qwerty = isQwertyMode(); 834 final int modifierState = event.getModifiers(); 835 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); 836 // Get the chars associated with the keyCode (i.e using any chording combo) 837 final boolean isKeyCodeMapped = event.getKeyData(possibleChars); 838 // The delete key is not mapped to '\b' so we treat it specially 839 if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { 840 return; 841 } 842 843 // Look for an item whose shortcut is this key. 844 final int N = mItems.size(); 845 for (int i = 0; i < N; i++) { 846 MenuItemImpl item = mItems.get(i); 847 if (item.hasSubMenu()) { 848 ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event); 849 } 850 final char shortcutChar = 851 qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); 852 final int shortcutModifiers = 853 qwerty ? item.getAlphabeticModifiers() : item.getNumericModifiers(); 854 final boolean isModifiersExactMatch = (modifierState & SUPPORTED_MODIFIERS_MASK) 855 == (shortcutModifiers & SUPPORTED_MODIFIERS_MASK); 856 if (isModifiersExactMatch && (shortcutChar != 0) && 857 (shortcutChar == possibleChars.meta[0] 858 || shortcutChar == possibleChars.meta[2] 859 || (qwerty && shortcutChar == '\b' && 860 keyCode == KeyEvent.KEYCODE_DEL)) && 861 item.isEnabled()) { 862 items.add(item); 863 } 864 } 865 } 866 867 /* 868 * We want to return the menu item associated with the key, but if there is no 869 * ambiguity (i.e. there is only one menu item corresponding to the key) we want 870 * to return it even if it's not an exact match; this allow the user to 871 * _not_ use the ALT key for example, making the use of shortcuts slightly more 872 * user-friendly. An example is on the G1, '!' and '1' are on the same key, and 873 * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut). 874 * 875 * On the other hand, if two (or more) shortcuts corresponds to the same key, 876 * we have to only return the exact match. 877 */ findItemWithShortcutForKey(int keyCode, KeyEvent event)878 MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { 879 // Get all items that can be associated directly or indirectly with the keyCode 880 ArrayList<MenuItemImpl> items = mTempShortcutItemList; 881 items.clear(); 882 findItemsWithShortcutForKey(items, keyCode, event); 883 884 if (items.isEmpty()) { 885 return null; 886 } 887 888 final int metaState = event.getMetaState(); 889 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); 890 // Get the chars associated with the keyCode (i.e using any chording combo) 891 event.getKeyData(possibleChars); 892 893 // If we have only one element, we can safely returns it 894 final int size = items.size(); 895 if (size == 1) { 896 return items.get(0); 897 } 898 899 final boolean qwerty = isQwertyMode(); 900 // If we found more than one item associated with the key, 901 // we have to return the exact match 902 for (int i = 0; i < size; i++) { 903 final MenuItemImpl item = items.get(i); 904 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : 905 item.getNumericShortcut(); 906 if ((shortcutChar == possibleChars.meta[0] && 907 (metaState & KeyEvent.META_ALT_ON) == 0) 908 || (shortcutChar == possibleChars.meta[2] && 909 (metaState & KeyEvent.META_ALT_ON) != 0) 910 || (qwerty && shortcutChar == '\b' && 911 keyCode == KeyEvent.KEYCODE_DEL)) { 912 return item; 913 } 914 } 915 return null; 916 } 917 performIdentifierAction(int id, int flags)918 public boolean performIdentifierAction(int id, int flags) { 919 // Look for an item whose identifier is the id. 920 return performItemAction(findItem(id), flags); 921 } 922 performItemAction(MenuItem item, int flags)923 public boolean performItemAction(MenuItem item, int flags) { 924 return performItemAction(item, null, flags); 925 } 926 performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags)927 public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) { 928 MenuItemImpl itemImpl = (MenuItemImpl) item; 929 930 if (itemImpl == null || !itemImpl.isEnabled()) { 931 return false; 932 } 933 934 boolean invoked = itemImpl.invoke(); 935 936 final ActionProvider provider = item.getActionProvider(); 937 final boolean providerHasSubMenu = provider != null && provider.hasSubMenu(); 938 if (itemImpl.hasCollapsibleActionView()) { 939 invoked |= itemImpl.expandActionView(); 940 if (invoked) { 941 close(true /* closeAllMenus */); 942 } 943 } else if (itemImpl.hasSubMenu() || providerHasSubMenu) { 944 if (!itemImpl.hasSubMenu()) { 945 itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl)); 946 } 947 948 final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu(); 949 if (providerHasSubMenu) { 950 provider.onPrepareSubMenu(subMenu); 951 } 952 invoked |= dispatchSubMenuSelected(subMenu, preferredPresenter); 953 if (!invoked) { 954 close(true /* closeAllMenus */); 955 } 956 } else { 957 if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { 958 close(true /* closeAllMenus */); 959 } 960 } 961 962 return invoked; 963 } 964 965 /** 966 * Closes the menu. 967 * 968 * @param closeAllMenus {@code true} if all displayed menus and submenus 969 * should be completely closed (as when a menu item is 970 * selected) or {@code false} if only this menu should 971 * be closed 972 */ close(boolean closeAllMenus)973 public final void close(boolean closeAllMenus) { 974 if (mIsClosing) return; 975 976 mIsClosing = true; 977 for (WeakReference<MenuPresenter> ref : mPresenters) { 978 final MenuPresenter presenter = ref.get(); 979 if (presenter == null) { 980 mPresenters.remove(ref); 981 } else { 982 presenter.onCloseMenu(this, closeAllMenus); 983 } 984 } 985 mIsClosing = false; 986 } 987 988 /** {@inheritDoc} */ close()989 public void close() { 990 close(true /* closeAllMenus */); 991 } 992 993 /** 994 * Called when an item is added or removed. 995 * 996 * @param structureChanged true if the menu structure changed, 997 * false if only item properties changed. 998 * (Visibility is a structural property since it affects layout.) 999 */ onItemsChanged(boolean structureChanged)1000 public void onItemsChanged(boolean structureChanged) { 1001 if (!mPreventDispatchingItemsChanged) { 1002 if (structureChanged) { 1003 mIsVisibleItemsStale = true; 1004 mIsActionItemsStale = true; 1005 } 1006 1007 dispatchPresenterUpdate(structureChanged); 1008 } else { 1009 mItemsChangedWhileDispatchPrevented = true; 1010 } 1011 } 1012 1013 /** 1014 * Stop dispatching item changed events to presenters until 1015 * {@link #startDispatchingItemsChanged()} is called. Useful when 1016 * many menu operations are going to be performed as a batch. 1017 */ 1018 @UnsupportedAppUsage stopDispatchingItemsChanged()1019 public void stopDispatchingItemsChanged() { 1020 if (!mPreventDispatchingItemsChanged) { 1021 mPreventDispatchingItemsChanged = true; 1022 mItemsChangedWhileDispatchPrevented = false; 1023 } 1024 } 1025 1026 @UnsupportedAppUsage startDispatchingItemsChanged()1027 public void startDispatchingItemsChanged() { 1028 mPreventDispatchingItemsChanged = false; 1029 1030 if (mItemsChangedWhileDispatchPrevented) { 1031 mItemsChangedWhileDispatchPrevented = false; 1032 onItemsChanged(true); 1033 } 1034 } 1035 1036 /** 1037 * Called by {@link MenuItemImpl} when its visible flag is changed. 1038 * @param item The item that has gone through a visibility change. 1039 */ onItemVisibleChanged(MenuItemImpl item)1040 void onItemVisibleChanged(MenuItemImpl item) { 1041 // Notify of items being changed 1042 mIsVisibleItemsStale = true; 1043 onItemsChanged(true); 1044 } 1045 1046 /** 1047 * Called by {@link MenuItemImpl} when its action request status is changed. 1048 * @param item The item that has gone through a change in action request status. 1049 */ onItemActionRequestChanged(MenuItemImpl item)1050 void onItemActionRequestChanged(MenuItemImpl item) { 1051 // Notify of items being changed 1052 mIsActionItemsStale = true; 1053 onItemsChanged(true); 1054 } 1055 1056 @NonNull 1057 @UnsupportedAppUsage getVisibleItems()1058 public ArrayList<MenuItemImpl> getVisibleItems() { 1059 if (!mIsVisibleItemsStale) return mVisibleItems; 1060 1061 // Refresh the visible items 1062 mVisibleItems.clear(); 1063 1064 final int itemsSize = mItems.size(); 1065 MenuItemImpl item; 1066 for (int i = 0; i < itemsSize; i++) { 1067 item = mItems.get(i); 1068 if (item.isVisible()) mVisibleItems.add(item); 1069 } 1070 1071 mIsVisibleItemsStale = false; 1072 mIsActionItemsStale = true; 1073 1074 return mVisibleItems; 1075 } 1076 1077 /** 1078 * This method determines which menu items get to be 'action items' that will appear 1079 * in an action bar and which items should be 'overflow items' in a secondary menu. 1080 * The rules are as follows: 1081 * 1082 * <p>Items are considered for inclusion in the order specified within the menu. 1083 * There is a limit of mMaxActionItems as a total count, optionally including the overflow 1084 * menu button itself. This is a soft limit; if an item shares a group ID with an item 1085 * previously included as an action item, the new item will stay with its group and become 1086 * an action item itself even if it breaks the max item count limit. This is done to 1087 * limit the conceptual complexity of the items presented within an action bar. Only a few 1088 * unrelated concepts should be presented to the user in this space, and groups are treated 1089 * as a single concept. 1090 * 1091 * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This 1092 * limit may be broken by a single item that exceeds the remaining space, but no further 1093 * items may be added. If an item that is part of a group cannot fit within the remaining 1094 * measured width, the entire group will be demoted to overflow. This is done to ensure room 1095 * for navigation and other affordances in the action bar as well as reduce general UI clutter. 1096 * 1097 * <p>The space freed by demoting a full group cannot be consumed by future menu items. 1098 * Once items begin to overflow, all future items become overflow items as well. This is 1099 * to avoid inadvertent reordering that may break the app's intended design. 1100 */ flagActionItems()1101 public void flagActionItems() { 1102 // Important side effect: if getVisibleItems is stale it may refresh, 1103 // which can affect action items staleness. 1104 final ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); 1105 1106 if (!mIsActionItemsStale) { 1107 return; 1108 } 1109 1110 // Presenters flag action items as needed. 1111 boolean flagged = false; 1112 for (WeakReference<MenuPresenter> ref : mPresenters) { 1113 final MenuPresenter presenter = ref.get(); 1114 if (presenter == null) { 1115 mPresenters.remove(ref); 1116 } else { 1117 flagged |= presenter.flagActionItems(); 1118 } 1119 } 1120 1121 if (flagged) { 1122 mActionItems.clear(); 1123 mNonActionItems.clear(); 1124 final int itemsSize = visibleItems.size(); 1125 for (int i = 0; i < itemsSize; i++) { 1126 MenuItemImpl item = visibleItems.get(i); 1127 if (item.isActionButton()) { 1128 mActionItems.add(item); 1129 } else { 1130 mNonActionItems.add(item); 1131 } 1132 } 1133 } else { 1134 // Nobody flagged anything, everything is a non-action item. 1135 // (This happens during a first pass with no action-item presenters.) 1136 mActionItems.clear(); 1137 mNonActionItems.clear(); 1138 mNonActionItems.addAll(getVisibleItems()); 1139 } 1140 mIsActionItemsStale = false; 1141 } 1142 getActionItems()1143 public ArrayList<MenuItemImpl> getActionItems() { 1144 flagActionItems(); 1145 return mActionItems; 1146 } 1147 1148 @UnsupportedAppUsage getNonActionItems()1149 public ArrayList<MenuItemImpl> getNonActionItems() { 1150 flagActionItems(); 1151 return mNonActionItems; 1152 } 1153 clearHeader()1154 public void clearHeader() { 1155 mHeaderIcon = null; 1156 mHeaderTitle = null; 1157 mHeaderView = null; 1158 1159 onItemsChanged(false); 1160 } 1161 setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, final Drawable icon, final View view)1162 private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, 1163 final Drawable icon, final View view) { 1164 final Resources r = getResources(); 1165 1166 if (view != null) { 1167 mHeaderView = view; 1168 1169 // If using a custom view, then the title and icon aren't used 1170 mHeaderTitle = null; 1171 mHeaderIcon = null; 1172 } else { 1173 if (titleRes > 0) { 1174 mHeaderTitle = r.getText(titleRes); 1175 } else if (title != null) { 1176 mHeaderTitle = title; 1177 } 1178 1179 if (iconRes > 0) { 1180 mHeaderIcon = getContext().getDrawable(iconRes); 1181 } else if (icon != null) { 1182 mHeaderIcon = icon; 1183 } 1184 1185 // If using the title or icon, then a custom view isn't used 1186 mHeaderView = null; 1187 } 1188 1189 // Notify of change 1190 onItemsChanged(false); 1191 } 1192 1193 /** 1194 * Sets the header's title. This replaces the header view. Called by the 1195 * builder-style methods of subclasses. 1196 * 1197 * @param title The new title. 1198 * @return This MenuBuilder so additional setters can be called. 1199 */ setHeaderTitleInt(CharSequence title)1200 protected MenuBuilder setHeaderTitleInt(CharSequence title) { 1201 setHeaderInternal(0, title, 0, null, null); 1202 return this; 1203 } 1204 1205 /** 1206 * Sets the header's title. This replaces the header view. Called by the 1207 * builder-style methods of subclasses. 1208 * 1209 * @param titleRes The new title (as a resource ID). 1210 * @return This MenuBuilder so additional setters can be called. 1211 */ setHeaderTitleInt(int titleRes)1212 protected MenuBuilder setHeaderTitleInt(int titleRes) { 1213 setHeaderInternal(titleRes, null, 0, null, null); 1214 return this; 1215 } 1216 1217 /** 1218 * Sets the header's icon. This replaces the header view. Called by the 1219 * builder-style methods of subclasses. 1220 * 1221 * @param icon The new icon. 1222 * @return This MenuBuilder so additional setters can be called. 1223 */ setHeaderIconInt(Drawable icon)1224 protected MenuBuilder setHeaderIconInt(Drawable icon) { 1225 setHeaderInternal(0, null, 0, icon, null); 1226 return this; 1227 } 1228 1229 /** 1230 * Sets the header's icon. This replaces the header view. Called by the 1231 * builder-style methods of subclasses. 1232 * 1233 * @param iconRes The new icon (as a resource ID). 1234 * @return This MenuBuilder so additional setters can be called. 1235 */ setHeaderIconInt(int iconRes)1236 protected MenuBuilder setHeaderIconInt(int iconRes) { 1237 setHeaderInternal(0, null, iconRes, null, null); 1238 return this; 1239 } 1240 1241 /** 1242 * Sets the header's view. This replaces the title and icon. Called by the 1243 * builder-style methods of subclasses. 1244 * 1245 * @param view The new view. 1246 * @return This MenuBuilder so additional setters can be called. 1247 */ setHeaderViewInt(View view)1248 protected MenuBuilder setHeaderViewInt(View view) { 1249 setHeaderInternal(0, null, 0, null, view); 1250 return this; 1251 } 1252 1253 @UnsupportedAppUsage getHeaderTitle()1254 public CharSequence getHeaderTitle() { 1255 return mHeaderTitle; 1256 } 1257 1258 @UnsupportedAppUsage getHeaderIcon()1259 public Drawable getHeaderIcon() { 1260 return mHeaderIcon; 1261 } 1262 getHeaderView()1263 public View getHeaderView() { 1264 return mHeaderView; 1265 } 1266 1267 /** 1268 * Gets the root menu (if this is a submenu, find its root menu). 1269 * @return The root menu. 1270 */ 1271 @UnsupportedAppUsage getRootMenu()1272 public MenuBuilder getRootMenu() { 1273 return this; 1274 } 1275 1276 /** 1277 * Sets the current menu info that is set on all items added to this menu 1278 * (until this is called again with different menu info, in which case that 1279 * one will be added to all subsequent item additions). 1280 * 1281 * @param menuInfo The extra menu information to add. 1282 */ 1283 @UnsupportedAppUsage setCurrentMenuInfo(ContextMenuInfo menuInfo)1284 public void setCurrentMenuInfo(ContextMenuInfo menuInfo) { 1285 mCurrentMenuInfo = menuInfo; 1286 } 1287 1288 @UnsupportedAppUsage setOptionalIconsVisible(boolean visible)1289 void setOptionalIconsVisible(boolean visible) { 1290 mOptionalIconsVisible = visible; 1291 } 1292 getOptionalIconsVisible()1293 boolean getOptionalIconsVisible() { 1294 return mOptionalIconsVisible; 1295 } 1296 expandItemActionView(MenuItemImpl item)1297 public boolean expandItemActionView(MenuItemImpl item) { 1298 if (mPresenters.isEmpty()) return false; 1299 1300 boolean expanded = false; 1301 1302 stopDispatchingItemsChanged(); 1303 for (WeakReference<MenuPresenter> ref : mPresenters) { 1304 final MenuPresenter presenter = ref.get(); 1305 if (presenter == null) { 1306 mPresenters.remove(ref); 1307 } else if ((expanded = presenter.expandItemActionView(this, item))) { 1308 break; 1309 } 1310 } 1311 startDispatchingItemsChanged(); 1312 1313 if (expanded) { 1314 mExpandedItem = item; 1315 } 1316 return expanded; 1317 } 1318 1319 @UnsupportedAppUsage collapseItemActionView(MenuItemImpl item)1320 public boolean collapseItemActionView(MenuItemImpl item) { 1321 if (mPresenters.isEmpty() || mExpandedItem != item) return false; 1322 1323 boolean collapsed = false; 1324 1325 stopDispatchingItemsChanged(); 1326 for (WeakReference<MenuPresenter> ref : mPresenters) { 1327 final MenuPresenter presenter = ref.get(); 1328 if (presenter == null) { 1329 mPresenters.remove(ref); 1330 } else if ((collapsed = presenter.collapseItemActionView(this, item))) { 1331 break; 1332 } 1333 } 1334 startDispatchingItemsChanged(); 1335 1336 if (collapsed) { 1337 mExpandedItem = null; 1338 } 1339 return collapsed; 1340 } 1341 getExpandedItem()1342 public MenuItemImpl getExpandedItem() { 1343 return mExpandedItem; 1344 } 1345 } 1346