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