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