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