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