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