• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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