• 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.ContextThemeWrapper;
32 import android.view.KeyCharacterMap;
33 import android.view.KeyEvent;
34 import android.view.Menu;
35 import android.view.MenuItem;
36 import android.view.SubMenu;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.LayoutInflater;
40 import android.view.ContextMenu.ContextMenuInfo;
41 import android.widget.AdapterView;
42 import android.widget.BaseAdapter;
43 
44 import java.lang.ref.WeakReference;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Vector;
48 
49 /**
50  * Implementation of the {@link android.view.Menu} interface for creating a
51  * standard menu UI.
52  */
53 public class MenuBuilder implements Menu {
54     private static final String LOGTAG = "MenuBuilder";
55 
56     /** The number of different menu types */
57     public static final int NUM_TYPES = 3;
58     /** The menu type that represents the icon menu view */
59     public static final int TYPE_ICON = 0;
60     /** The menu type that represents the expanded menu view */
61     public static final int TYPE_EXPANDED = 1;
62     /**
63      * The menu type that represents a menu dialog. Examples are context and sub
64      * menus. This menu type will not have a corresponding MenuView, but it will
65      * have an ItemView.
66      */
67     public static final int TYPE_DIALOG = 2;
68 
69     private static final String VIEWS_TAG = "android:views";
70 
71     // Order must be the same order as the TYPE_*
72     static final int THEME_RES_FOR_TYPE[] = new int[] {
73         com.android.internal.R.style.Theme_IconMenu,
74         com.android.internal.R.style.Theme_ExpandedMenu,
75         0,
76     };
77 
78     // Order must be the same order as the TYPE_*
79     static final int LAYOUT_RES_FOR_TYPE[] = new int[] {
80         com.android.internal.R.layout.icon_menu_layout,
81         com.android.internal.R.layout.expanded_menu_layout,
82         0,
83     };
84 
85     // Order must be the same order as the TYPE_*
86     static final int ITEM_LAYOUT_RES_FOR_TYPE[] = new int[] {
87         com.android.internal.R.layout.icon_menu_item_layout,
88         com.android.internal.R.layout.list_menu_item_layout,
89         com.android.internal.R.layout.list_menu_item_layout,
90     };
91 
92     private static final int[]  sCategoryToOrder = new int[] {
93         1, /* No category */
94         4, /* CONTAINER */
95         5, /* SYSTEM */
96         3, /* SECONDARY */
97         2, /* ALTERNATIVE */
98         0, /* SELECTED_ALTERNATIVE */
99     };
100 
101     private final Context mContext;
102     private final Resources mResources;
103 
104     /**
105      * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
106      * instead of accessing this directly.
107      */
108     private boolean mQwertyMode;
109 
110     /**
111      * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
112      * instead of accessing this directly.
113      */
114     private boolean mShortcutsVisible;
115 
116     /**
117      * Callback that will receive the various menu-related events generated by
118      * this class. Use getCallback to get a reference to the callback.
119      */
120     private Callback mCallback;
121 
122     /** Contains all of the items for this menu */
123     private ArrayList<MenuItemImpl> mItems;
124 
125     /** Contains only the items that are currently visible.  This will be created/refreshed from
126      * {@link #getVisibleItems()} */
127     private ArrayList<MenuItemImpl> mVisibleItems;
128     /**
129      * Whether or not the items (or any one item's shown state) has changed since it was last
130      * fetched from {@link #getVisibleItems()}
131      */
132     private boolean mIsVisibleItemsStale;
133 
134     /**
135      * Current use case is Context Menus: As Views populate the context menu, each one has
136      * extra information that should be passed along.  This is the current menu info that
137      * should be set on all items added to this menu.
138      */
139     private ContextMenuInfo mCurrentMenuInfo;
140 
141     /** Header title for menu types that have a header (context and submenus) */
142     CharSequence mHeaderTitle;
143     /** Header icon for menu types that have a header and support icons (context) */
144     Drawable mHeaderIcon;
145     /** Header custom view for menu types that have a header and support custom views (context) */
146     View mHeaderView;
147 
148     /**
149      * Contains the state of the View hierarchy for all menu views when the menu
150      * was frozen.
151      */
152     private SparseArray<Parcelable> mFrozenViewStates;
153 
154     /**
155      * Prevents onItemsChanged from doing its junk, useful for batching commands
156      * that may individually call onItemsChanged.
157      */
158     private boolean mPreventDispatchingItemsChanged = false;
159 
160     private boolean mOptionalIconsVisible = false;
161 
162     private MenuType[] mMenuTypes;
163     class MenuType {
164         private int mMenuType;
165 
166         /** The layout inflater that uses the menu type's theme */
167         private LayoutInflater mInflater;
168 
169         /** The lazily loaded {@link MenuView} */
170         private WeakReference<MenuView> mMenuView;
171 
MenuType(int menuType)172         MenuType(int menuType) {
173             mMenuType = menuType;
174         }
175 
getInflater()176         LayoutInflater getInflater() {
177             // Create an inflater that uses the given theme for the Views it inflates
178             if (mInflater == null) {
179                 Context wrappedContext = new ContextThemeWrapper(mContext,
180                         THEME_RES_FOR_TYPE[mMenuType]);
181                 mInflater = (LayoutInflater) wrappedContext
182                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
183             }
184 
185             return mInflater;
186         }
187 
getMenuView(ViewGroup parent)188         MenuView getMenuView(ViewGroup parent) {
189             if (LAYOUT_RES_FOR_TYPE[mMenuType] == 0) {
190                 return null;
191             }
192 
193             synchronized (this) {
194                 MenuView menuView = mMenuView != null ? mMenuView.get() : null;
195 
196                 if (menuView == null) {
197                     menuView = (MenuView) getInflater().inflate(
198                             LAYOUT_RES_FOR_TYPE[mMenuType], parent, false);
199                     menuView.initialize(MenuBuilder.this, mMenuType);
200 
201                     // Cache the view
202                     mMenuView = new WeakReference<MenuView>(menuView);
203 
204                     if (mFrozenViewStates != null) {
205                         View view = (View) menuView;
206                         view.restoreHierarchyState(mFrozenViewStates);
207 
208                         // Clear this menu type's frozen state, since we just restored it
209                         mFrozenViewStates.remove(view.getId());
210                     }
211                 }
212 
213                 return menuView;
214             }
215         }
216 
hasMenuView()217         boolean hasMenuView() {
218             return mMenuView != null && mMenuView.get() != null;
219         }
220     }
221 
222     /**
223      * Called by menu to notify of close and selection changes
224      */
225     public interface Callback {
226         /**
227          * Called when a menu item is selected.
228          * @param menu The menu that is the parent of the item
229          * @param item The menu item that is selected
230          * @return whether the menu item selection was handled
231          */
onMenuItemSelected(MenuBuilder menu, MenuItem item)232         public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
233 
234         /**
235          * Called when a menu is closed.
236          * @param menu The menu that was closed.
237          * @param allMenusAreClosing Whether the menus are completely closing (true),
238          *            or whether there is another menu opening shortly
239          *            (false). For example, if the menu is closing because a
240          *            sub menu is about to be shown, <var>allMenusAreClosing</var>
241          *            is false.
242          */
onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing)243         public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing);
244 
245         /**
246          * Called when a sub menu is selected.  This is a cue to open the given sub menu's decor.
247          * @param subMenu the sub menu that is being opened
248          * @return whether the sub menu selection was handled by the callback
249          */
onSubMenuSelected(SubMenuBuilder subMenu)250         public boolean onSubMenuSelected(SubMenuBuilder subMenu);
251 
252         /**
253          * Called when a sub menu is closed
254          * @param menu the sub menu that was closed
255          */
onCloseSubMenu(SubMenuBuilder menu)256         public void onCloseSubMenu(SubMenuBuilder menu);
257 
258         /**
259          * Called when the mode of the menu changes (for example, from icon to expanded).
260          *
261          * @param menu the menu that has changed modes
262          */
onMenuModeChange(MenuBuilder menu)263         public void onMenuModeChange(MenuBuilder menu);
264     }
265 
266     /**
267      * Called by menu items to execute their associated action
268      */
269     public interface ItemInvoker {
invokeItem(MenuItemImpl item)270         public boolean invokeItem(MenuItemImpl item);
271     }
272 
MenuBuilder(Context context)273     public MenuBuilder(Context context) {
274         mMenuTypes = new MenuType[NUM_TYPES];
275 
276         mContext = context;
277         mResources = context.getResources();
278 
279         mItems = new ArrayList<MenuItemImpl>();
280 
281         mVisibleItems = new ArrayList<MenuItemImpl>();
282         mIsVisibleItemsStale = true;
283 
284         mShortcutsVisible =
285                 (mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS);
286     }
287 
setCallback(Callback callback)288     public void setCallback(Callback callback) {
289         mCallback = callback;
290     }
291 
getMenuType(int menuType)292     MenuType getMenuType(int menuType) {
293         if (mMenuTypes[menuType] == null) {
294             mMenuTypes[menuType] = new MenuType(menuType);
295         }
296 
297         return mMenuTypes[menuType];
298     }
299 
300     /**
301      * Gets a menu View that contains this menu's items.
302      *
303      * @param menuType The type of menu to get a View for (must be one of
304      *            {@link #TYPE_ICON}, {@link #TYPE_EXPANDED},
305      *            {@link #TYPE_DIALOG}).
306      * @param parent The ViewGroup that provides a set of LayoutParams values
307      *            for this menu view
308      * @return A View for the menu of type <var>menuType</var>
309      */
getMenuView(int menuType, ViewGroup parent)310     public View getMenuView(int menuType, ViewGroup parent) {
311         // The expanded menu depends on the number if items shown in the icon menu (which
312         // is adjustable as setters/XML attributes on IconMenuView [imagine a larger LCD
313         // wanting to show more icons]). If, for example, the activity goes through
314         // an orientation change while the expanded menu is open, the icon menu's view
315         // won't have an instance anymore; so here we make sure we have an icon menu view (matching
316         // the same parent so the layout parameters from the XML are used). This
317         // will create the icon menu view and cache it (if it doesn't already exist).
318         if (menuType == TYPE_EXPANDED
319                 && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) {
320             getMenuType(TYPE_ICON).getMenuView(parent);
321         }
322 
323         return (View) getMenuType(menuType).getMenuView(parent);
324     }
325 
getNumIconMenuItemsShown()326     private int getNumIconMenuItemsShown() {
327         ViewGroup parent = null;
328 
329         if (!mMenuTypes[TYPE_ICON].hasMenuView()) {
330             /*
331              * There isn't an icon menu view instantiated, so when we get it
332              * below, it will lazily instantiate it. We should pass a proper
333              * parent so it uses the layout_ attributes present in the XML
334              * layout file.
335              */
336             if (mMenuTypes[TYPE_EXPANDED].hasMenuView()) {
337                 View expandedMenuView = (View) mMenuTypes[TYPE_EXPANDED].getMenuView(null);
338                 parent = (ViewGroup) expandedMenuView.getParent();
339             }
340         }
341 
342         return ((IconMenuView) getMenuView(TYPE_ICON, parent)).getNumActualItemsShown();
343     }
344 
345     /**
346      * Clears the cached menu views. Call this if the menu views need to another
347      * layout (for example, if the screen size has changed).
348      */
clearMenuViews()349     public void clearMenuViews() {
350         for (int i = NUM_TYPES - 1; i >= 0; i--) {
351             if (mMenuTypes[i] != null) {
352                 mMenuTypes[i].mMenuView = null;
353             }
354         }
355 
356         for (int i = mItems.size() - 1; i >= 0; i--) {
357             MenuItemImpl item = mItems.get(i);
358             if (item.hasSubMenu()) {
359                 ((SubMenuBuilder) item.getSubMenu()).clearMenuViews();
360             }
361             item.clearItemViews();
362         }
363     }
364 
365     /**
366      * Adds an item to the menu.  The other add methods funnel to this.
367      */
addInternal(int group, int id, int categoryOrder, CharSequence title)368     private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
369         final int ordering = getOrdering(categoryOrder);
370 
371         final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder, ordering, title);
372 
373         if (mCurrentMenuInfo != null) {
374             // Pass along the current menu info
375             item.setMenuInfo(mCurrentMenuInfo);
376         }
377 
378         mItems.add(findInsertIndex(mItems, ordering), item);
379         onItemsChanged(false);
380 
381         return item;
382     }
383 
add(CharSequence title)384     public MenuItem add(CharSequence title) {
385         return addInternal(0, 0, 0, title);
386     }
387 
add(int titleRes)388     public MenuItem add(int titleRes) {
389         return addInternal(0, 0, 0, mResources.getString(titleRes));
390     }
391 
add(int group, int id, int categoryOrder, CharSequence title)392     public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
393         return addInternal(group, id, categoryOrder, title);
394     }
395 
add(int group, int id, int categoryOrder, int title)396     public MenuItem add(int group, int id, int categoryOrder, int title) {
397         return addInternal(group, id, categoryOrder, mResources.getString(title));
398     }
399 
addSubMenu(CharSequence title)400     public SubMenu addSubMenu(CharSequence title) {
401         return addSubMenu(0, 0, 0, title);
402     }
403 
addSubMenu(int titleRes)404     public SubMenu addSubMenu(int titleRes) {
405         return addSubMenu(0, 0, 0, mResources.getString(titleRes));
406     }
407 
addSubMenu(int group, int id, int categoryOrder, CharSequence title)408     public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
409         final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
410         final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
411         item.setSubMenu(subMenu);
412 
413         return subMenu;
414     }
415 
addSubMenu(int group, int id, int categoryOrder, int title)416     public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
417         return addSubMenu(group, id, categoryOrder, mResources.getString(title));
418     }
419 
addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems)420     public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
421             Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
422         PackageManager pm = mContext.getPackageManager();
423         final List<ResolveInfo> lri =
424                 pm.queryIntentActivityOptions(caller, specifics, intent, 0);
425         final int N = lri != null ? lri.size() : 0;
426 
427         if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
428             removeGroup(group);
429         }
430 
431         for (int i=0; i<N; i++) {
432             final ResolveInfo ri = lri.get(i);
433             Intent rintent = new Intent(
434                 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
435             rintent.setComponent(new ComponentName(
436                     ri.activityInfo.applicationInfo.packageName,
437                     ri.activityInfo.name));
438             final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
439                     .setIcon(ri.loadIcon(pm))
440                     .setIntent(rintent);
441             if (outSpecificItems != null && ri.specificIndex >= 0) {
442                 outSpecificItems[ri.specificIndex] = item;
443             }
444         }
445 
446         return N;
447     }
448 
removeItem(int id)449     public void removeItem(int id) {
450         removeItemAtInt(findItemIndex(id), true);
451     }
452 
removeGroup(int group)453     public void removeGroup(int group) {
454         final int i = findGroupIndex(group);
455 
456         if (i >= 0) {
457             final int maxRemovable = mItems.size() - i;
458             int numRemoved = 0;
459             while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
460                 // Don't force update for each one, this method will do it at the end
461                 removeItemAtInt(i, false);
462             }
463 
464             // Notify menu views
465             onItemsChanged(false);
466         }
467     }
468 
469     /**
470      * Remove the item at the given index and optionally forces menu views to
471      * update.
472      *
473      * @param index The index of the item to be removed. If this index is
474      *            invalid an exception is thrown.
475      * @param updateChildrenOnMenuViews Whether to force update on menu views.
476      *            Please make sure you eventually call this after your batch of
477      *            removals.
478      */
removeItemAtInt(int index, boolean updateChildrenOnMenuViews)479     private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
480         if ((index < 0) || (index >= mItems.size())) return;
481 
482         mItems.remove(index);
483 
484         if (updateChildrenOnMenuViews) onItemsChanged(false);
485     }
486 
removeItemAt(int index)487     public void removeItemAt(int index) {
488         removeItemAtInt(index, true);
489     }
490 
clearAll()491     public void clearAll() {
492         mPreventDispatchingItemsChanged = true;
493         clear();
494         clearHeader();
495         mPreventDispatchingItemsChanged = false;
496         onItemsChanged(true);
497     }
498 
clear()499     public void clear() {
500         mItems.clear();
501 
502         onItemsChanged(true);
503     }
504 
setExclusiveItemChecked(MenuItem item)505     void setExclusiveItemChecked(MenuItem item) {
506         final int group = item.getGroupId();
507 
508         final int N = mItems.size();
509         for (int i = 0; i < N; i++) {
510             MenuItemImpl curItem = mItems.get(i);
511             if (curItem.getGroupId() == group) {
512                 if (!curItem.isExclusiveCheckable()) continue;
513                 if (!curItem.isCheckable()) continue;
514 
515                 // Check the item meant to be checked, uncheck the others (that are in the group)
516                 curItem.setCheckedInt(curItem == item);
517             }
518         }
519     }
520 
setGroupCheckable(int group, boolean checkable, boolean exclusive)521     public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
522         final int N = mItems.size();
523 
524         for (int i = 0; i < N; i++) {
525             MenuItemImpl item = mItems.get(i);
526             if (item.getGroupId() == group) {
527                 item.setExclusiveCheckable(exclusive);
528                 item.setCheckable(checkable);
529             }
530         }
531     }
532 
setGroupVisible(int group, boolean visible)533     public void setGroupVisible(int group, boolean visible) {
534         final int N = mItems.size();
535 
536         // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
537         // than setVisible and at the end notify of items being changed
538 
539         boolean changedAtLeastOneItem = false;
540         for (int i = 0; i < N; i++) {
541             MenuItemImpl item = mItems.get(i);
542             if (item.getGroupId() == group) {
543                 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
544             }
545         }
546 
547         if (changedAtLeastOneItem) onItemsChanged(false);
548     }
549 
setGroupEnabled(int group, boolean enabled)550     public void setGroupEnabled(int group, boolean enabled) {
551         final int N = mItems.size();
552 
553         for (int i = 0; i < N; i++) {
554             MenuItemImpl item = mItems.get(i);
555             if (item.getGroupId() == group) {
556                 item.setEnabled(enabled);
557             }
558         }
559     }
560 
hasVisibleItems()561     public boolean hasVisibleItems() {
562         final int size = size();
563 
564         for (int i = 0; i < size; i++) {
565             MenuItemImpl item = mItems.get(i);
566             if (item.isVisible()) {
567                 return true;
568             }
569         }
570 
571         return false;
572     }
573 
findItem(int id)574     public MenuItem findItem(int id) {
575         final int size = size();
576         for (int i = 0; i < size; i++) {
577             MenuItemImpl item = mItems.get(i);
578             if (item.getItemId() == id) {
579                 return item;
580             } else if (item.hasSubMenu()) {
581                 MenuItem possibleItem = item.getSubMenu().findItem(id);
582 
583                 if (possibleItem != null) {
584                     return possibleItem;
585                 }
586             }
587         }
588 
589         return null;
590     }
591 
findItemIndex(int id)592     public int findItemIndex(int id) {
593         final int size = size();
594 
595         for (int i = 0; i < size; i++) {
596             MenuItemImpl item = mItems.get(i);
597             if (item.getItemId() == id) {
598                 return i;
599             }
600         }
601 
602         return -1;
603     }
604 
findGroupIndex(int group)605     public int findGroupIndex(int group) {
606         return findGroupIndex(group, 0);
607     }
608 
findGroupIndex(int group, int start)609     public int findGroupIndex(int group, int start) {
610         final int size = size();
611 
612         if (start < 0) {
613             start = 0;
614         }
615 
616         for (int i = start; i < size; i++) {
617             final MenuItemImpl item = mItems.get(i);
618 
619             if (item.getGroupId() == group) {
620                 return i;
621             }
622         }
623 
624         return -1;
625     }
626 
size()627     public int size() {
628         return mItems.size();
629     }
630 
631     /** {@inheritDoc} */
getItem(int index)632     public MenuItem getItem(int index) {
633         return mItems.get(index);
634     }
635 
isShortcutKey(int keyCode, KeyEvent event)636     public boolean isShortcutKey(int keyCode, KeyEvent event) {
637         return findItemWithShortcutForKey(keyCode, event) != null;
638     }
639 
setQwertyMode(boolean isQwerty)640     public void setQwertyMode(boolean isQwerty) {
641         mQwertyMode = isQwerty;
642 
643         refreshShortcuts(isShortcutsVisible(), isQwerty);
644     }
645 
646     /**
647      * Returns the ordering across all items. This will grab the category from
648      * the upper bits, find out how to order the category with respect to other
649      * categories, and combine it with the lower bits.
650      *
651      * @param categoryOrder The category order for a particular item (if it has
652      *            not been or/add with a category, the default category is
653      *            assumed).
654      * @return An ordering integer that can be used to order this item across
655      *         all the items (even from other categories).
656      */
getOrdering(int categoryOrder)657     private static int getOrdering(int categoryOrder)
658     {
659         final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
660 
661         if (index < 0 || index >= sCategoryToOrder.length) {
662             throw new IllegalArgumentException("order does not contain a valid category.");
663         }
664 
665         return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
666     }
667 
668     /**
669      * @return whether the menu shortcuts are in qwerty mode or not
670      */
isQwertyMode()671     boolean isQwertyMode() {
672         return mQwertyMode;
673     }
674 
675     /**
676      * Refreshes the shortcut labels on each of the displayed items.  Passes the arguments
677      * so submenus don't need to call their parent menu for the same values.
678      */
refreshShortcuts(boolean shortcutsVisible, boolean qwertyMode)679     private void refreshShortcuts(boolean shortcutsVisible, boolean qwertyMode) {
680         MenuItemImpl item;
681         for (int i = mItems.size() - 1; i >= 0; i--) {
682             item = mItems.get(i);
683 
684             if (item.hasSubMenu()) {
685                 ((MenuBuilder) item.getSubMenu()).refreshShortcuts(shortcutsVisible, qwertyMode);
686             }
687 
688             item.refreshShortcutOnItemViews(shortcutsVisible, qwertyMode);
689         }
690     }
691 
692     /**
693      * Sets whether the shortcuts should be visible on menus.  Devices without hardware
694      * key input will never make shortcuts visible even if this method is passed 'true'.
695      *
696      * @param shortcutsVisible Whether shortcuts should be visible (if true and a
697      *            menu item does not have a shortcut defined, that item will
698      *            still NOT show a shortcut)
699      */
setShortcutsVisible(boolean shortcutsVisible)700     public void setShortcutsVisible(boolean shortcutsVisible) {
701         if (mShortcutsVisible == shortcutsVisible) return;
702 
703         mShortcutsVisible =
704             (mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS)
705             && shortcutsVisible;
706 
707         refreshShortcuts(mShortcutsVisible, isQwertyMode());
708     }
709 
710     /**
711      * @return Whether shortcuts should be visible on menus.
712      */
isShortcutsVisible()713     public boolean isShortcutsVisible() {
714         return mShortcutsVisible;
715     }
716 
getResources()717     Resources getResources() {
718         return mResources;
719     }
720 
getCallback()721     public Callback getCallback() {
722         return mCallback;
723     }
724 
getContext()725     public Context getContext() {
726         return mContext;
727     }
728 
findInsertIndex(ArrayList<MenuItemImpl> items, int ordering)729     private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
730         for (int i = items.size() - 1; i >= 0; i--) {
731             MenuItemImpl item = items.get(i);
732             if (item.getOrdering() <= ordering) {
733                 return i + 1;
734             }
735         }
736 
737         return 0;
738     }
739 
performShortcut(int keyCode, KeyEvent event, int flags)740     public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
741         final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
742 
743         boolean handled = false;
744 
745         if (item != null) {
746             handled = performItemAction(item, flags);
747         }
748 
749         if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
750             close(true);
751         }
752 
753         return handled;
754     }
755 
756     /*
757      * This function will return all the menu and sub-menu items that can
758      * be directly (the shortcut directly corresponds) and indirectly
759      * (the ALT-enabled char corresponds to the shortcut) associated
760      * with the keyCode.
761      */
findItemsWithShortcutForKey(int keyCode, KeyEvent event)762     List<MenuItemImpl> findItemsWithShortcutForKey(int keyCode, KeyEvent event) {
763         final boolean qwerty = isQwertyMode();
764         final int metaState = event.getMetaState();
765         final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
766         // Get the chars associated with the keyCode (i.e using any chording combo)
767         final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
768         // The delete key is not mapped to '\b' so we treat it specially
769         if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
770             return null;
771         }
772 
773         Vector<MenuItemImpl> items = new Vector();
774         // Look for an item whose shortcut is this key.
775         final int N = mItems.size();
776         for (int i = 0; i < N; i++) {
777             MenuItemImpl item = mItems.get(i);
778             if (item.hasSubMenu()) {
779                 List<MenuItemImpl> subMenuItems = ((MenuBuilder)item.getSubMenu())
780                     .findItemsWithShortcutForKey(keyCode, event);
781                 items.addAll(subMenuItems);
782             }
783             final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
784             if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) &&
785                   (shortcutChar != 0) &&
786                   (shortcutChar == possibleChars.meta[0]
787                       || shortcutChar == possibleChars.meta[2]
788                       || (qwerty && shortcutChar == '\b' &&
789                           keyCode == KeyEvent.KEYCODE_DEL)) &&
790                   item.isEnabled()) {
791                 items.add(item);
792             }
793         }
794         return items;
795     }
796 
797     /*
798      * We want to return the menu item associated with the key, but if there is no
799      * ambiguity (i.e. there is only one menu item corresponding to the key) we want
800      * to return it even if it's not an exact match; this allow the user to
801      * _not_ use the ALT key for example, making the use of shortcuts slightly more
802      * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
803      * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
804      *
805      * On the other hand, if two (or more) shortcuts corresponds to the same key,
806      * we have to only return the exact match.
807      */
findItemWithShortcutForKey(int keyCode, KeyEvent event)808     MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
809         // Get all items that can be associated directly or indirectly with the keyCode
810         List<MenuItemImpl> items = findItemsWithShortcutForKey(keyCode, event);
811 
812         if (items == null) {
813             return null;
814         }
815 
816         final int metaState = event.getMetaState();
817         final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
818         // Get the chars associated with the keyCode (i.e using any chording combo)
819         event.getKeyData(possibleChars);
820 
821         // If we have only one element, we can safely returns it
822         if (items.size() == 1) {
823             return items.get(0);
824         }
825 
826         final boolean qwerty = isQwertyMode();
827         // If we found more than one item associated with the key,
828         // we have to return the exact match
829         for (MenuItemImpl item : items) {
830             final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
831             if ((shortcutChar == possibleChars.meta[0] &&
832                     (metaState & KeyEvent.META_ALT_ON) == 0)
833                 || (shortcutChar == possibleChars.meta[2] &&
834                     (metaState & KeyEvent.META_ALT_ON) != 0)
835                 || (qwerty && shortcutChar == '\b' &&
836                     keyCode == KeyEvent.KEYCODE_DEL)) {
837                 return item;
838             }
839         }
840         return null;
841     }
842 
performIdentifierAction(int id, int flags)843     public boolean performIdentifierAction(int id, int flags) {
844         // Look for an item whose identifier is the id.
845         return performItemAction(findItem(id), flags);
846     }
847 
performItemAction(MenuItem item, int flags)848     public boolean performItemAction(MenuItem item, int flags) {
849         MenuItemImpl itemImpl = (MenuItemImpl) item;
850 
851         if (itemImpl == null || !itemImpl.isEnabled()) {
852             return false;
853         }
854 
855         boolean invoked = itemImpl.invoke();
856 
857         if (item.hasSubMenu()) {
858             close(false);
859 
860             if (mCallback != null) {
861                 // Return true if the sub menu was invoked or the item was invoked previously
862                 invoked = mCallback.onSubMenuSelected((SubMenuBuilder) item.getSubMenu())
863                         || invoked;
864             }
865         } else {
866             if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
867                 close(true);
868             }
869         }
870 
871         return invoked;
872     }
873 
874     /**
875      * Closes the visible menu.
876      *
877      * @param allMenusAreClosing Whether the menus are completely closing (true),
878      *            or whether there is another menu coming in this menu's place
879      *            (false). For example, if the menu is closing because a
880      *            sub menu is about to be shown, <var>allMenusAreClosing</var>
881      *            is false.
882      */
close(boolean allMenusAreClosing)883     final void close(boolean allMenusAreClosing) {
884         Callback callback = getCallback();
885         if (callback != null) {
886             callback.onCloseMenu(this, allMenusAreClosing);
887         }
888     }
889 
890     /** {@inheritDoc} */
close()891     public void close() {
892         close(true);
893     }
894 
895     /**
896      * Called when an item is added or removed.
897      *
898      * @param cleared Whether the items were cleared or just changed.
899      */
onItemsChanged(boolean cleared)900     private void onItemsChanged(boolean cleared) {
901         if (!mPreventDispatchingItemsChanged) {
902             if (mIsVisibleItemsStale == false) mIsVisibleItemsStale = true;
903 
904             MenuType[] menuTypes = mMenuTypes;
905             for (int i = 0; i < NUM_TYPES; i++) {
906                 if ((menuTypes[i] != null) && (menuTypes[i].hasMenuView())) {
907                     MenuView menuView = menuTypes[i].mMenuView.get();
908                     menuView.updateChildren(cleared);
909                 }
910             }
911         }
912     }
913 
914     /**
915      * Called by {@link MenuItemImpl} when its visible flag is changed.
916      * @param item The item that has gone through a visibility change.
917      */
onItemVisibleChanged(MenuItemImpl item)918     void onItemVisibleChanged(MenuItemImpl item) {
919         // Notify of items being changed
920         onItemsChanged(false);
921     }
922 
getVisibleItems()923     ArrayList<MenuItemImpl> getVisibleItems() {
924         if (!mIsVisibleItemsStale) return mVisibleItems;
925 
926         // Refresh the visible items
927         mVisibleItems.clear();
928 
929         final int itemsSize = mItems.size();
930         MenuItemImpl item;
931         for (int i = 0; i < itemsSize; i++) {
932             item = mItems.get(i);
933             if (item.isVisible()) mVisibleItems.add(item);
934         }
935 
936         mIsVisibleItemsStale = false;
937 
938         return mVisibleItems;
939     }
940 
clearHeader()941     public void clearHeader() {
942         mHeaderIcon = null;
943         mHeaderTitle = null;
944         mHeaderView = null;
945 
946         onItemsChanged(false);
947     }
948 
setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, final Drawable icon, final View view)949     private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
950             final Drawable icon, final View view) {
951         final Resources r = getResources();
952 
953         if (view != null) {
954             mHeaderView = view;
955 
956             // If using a custom view, then the title and icon aren't used
957             mHeaderTitle = null;
958             mHeaderIcon = null;
959         } else {
960             if (titleRes > 0) {
961                 mHeaderTitle = r.getText(titleRes);
962             } else if (title != null) {
963                 mHeaderTitle = title;
964             }
965 
966             if (iconRes > 0) {
967                 mHeaderIcon = r.getDrawable(iconRes);
968             } else if (icon != null) {
969                 mHeaderIcon = icon;
970             }
971 
972             // If using the title or icon, then a custom view isn't used
973             mHeaderView = null;
974         }
975 
976         // Notify of change
977         onItemsChanged(false);
978     }
979 
980     /**
981      * Sets the header's title. This replaces the header view. Called by the
982      * builder-style methods of subclasses.
983      *
984      * @param title The new title.
985      * @return This MenuBuilder so additional setters can be called.
986      */
setHeaderTitleInt(CharSequence title)987     protected MenuBuilder setHeaderTitleInt(CharSequence title) {
988         setHeaderInternal(0, title, 0, null, null);
989         return this;
990     }
991 
992     /**
993      * Sets the header's title. This replaces the header view. Called by the
994      * builder-style methods of subclasses.
995      *
996      * @param titleRes The new title (as a resource ID).
997      * @return This MenuBuilder so additional setters can be called.
998      */
setHeaderTitleInt(int titleRes)999     protected MenuBuilder setHeaderTitleInt(int titleRes) {
1000         setHeaderInternal(titleRes, null, 0, null, null);
1001         return this;
1002     }
1003 
1004     /**
1005      * Sets the header's icon. This replaces the header view. Called by the
1006      * builder-style methods of subclasses.
1007      *
1008      * @param icon The new icon.
1009      * @return This MenuBuilder so additional setters can be called.
1010      */
setHeaderIconInt(Drawable icon)1011     protected MenuBuilder setHeaderIconInt(Drawable icon) {
1012         setHeaderInternal(0, null, 0, icon, null);
1013         return this;
1014     }
1015 
1016     /**
1017      * Sets the header's icon. This replaces the header view. Called by the
1018      * builder-style methods of subclasses.
1019      *
1020      * @param iconRes The new icon (as a resource ID).
1021      * @return This MenuBuilder so additional setters can be called.
1022      */
setHeaderIconInt(int iconRes)1023     protected MenuBuilder setHeaderIconInt(int iconRes) {
1024         setHeaderInternal(0, null, iconRes, null, null);
1025         return this;
1026     }
1027 
1028     /**
1029      * Sets the header's view. This replaces the title and icon. Called by the
1030      * builder-style methods of subclasses.
1031      *
1032      * @param view The new view.
1033      * @return This MenuBuilder so additional setters can be called.
1034      */
setHeaderViewInt(View view)1035     protected MenuBuilder setHeaderViewInt(View view) {
1036         setHeaderInternal(0, null, 0, null, view);
1037         return this;
1038     }
1039 
getHeaderTitle()1040     public CharSequence getHeaderTitle() {
1041         return mHeaderTitle;
1042     }
1043 
getHeaderIcon()1044     public Drawable getHeaderIcon() {
1045         return mHeaderIcon;
1046     }
1047 
getHeaderView()1048     public View getHeaderView() {
1049         return mHeaderView;
1050     }
1051 
1052     /**
1053      * Gets the root menu (if this is a submenu, find its root menu).
1054      * @return The root menu.
1055      */
getRootMenu()1056     public MenuBuilder getRootMenu() {
1057         return this;
1058     }
1059 
1060     /**
1061      * Sets the current menu info that is set on all items added to this menu
1062      * (until this is called again with different menu info, in which case that
1063      * one will be added to all subsequent item additions).
1064      *
1065      * @param menuInfo The extra menu information to add.
1066      */
setCurrentMenuInfo(ContextMenuInfo menuInfo)1067     public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
1068         mCurrentMenuInfo = menuInfo;
1069     }
1070 
1071     /**
1072      * Gets an adapter for providing items and their views.
1073      *
1074      * @param menuType The type of menu to get an adapter for.
1075      * @return A {@link MenuAdapter} for this menu with the given menu type.
1076      */
getMenuAdapter(int menuType)1077     public MenuAdapter getMenuAdapter(int menuType) {
1078         return new MenuAdapter(menuType);
1079     }
1080 
setOptionalIconsVisible(boolean visible)1081     void setOptionalIconsVisible(boolean visible) {
1082         mOptionalIconsVisible = visible;
1083     }
1084 
getOptionalIconsVisible()1085     boolean getOptionalIconsVisible() {
1086         return mOptionalIconsVisible;
1087     }
1088 
saveHierarchyState(Bundle outState)1089     public void saveHierarchyState(Bundle outState) {
1090         SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>();
1091 
1092         MenuType[] menuTypes = mMenuTypes;
1093         for (int i = NUM_TYPES - 1; i >= 0; i--) {
1094             if (menuTypes[i] == null) {
1095                 continue;
1096             }
1097 
1098             if (menuTypes[i].hasMenuView()) {
1099                 ((View) menuTypes[i].getMenuView(null)).saveHierarchyState(viewStates);
1100             }
1101         }
1102 
1103         outState.putSparseParcelableArray(VIEWS_TAG, viewStates);
1104     }
1105 
restoreHierarchyState(Bundle inState)1106     public void restoreHierarchyState(Bundle inState) {
1107         // Save this for menu views opened later
1108         SparseArray<Parcelable> viewStates = mFrozenViewStates = inState
1109                 .getSparseParcelableArray(VIEWS_TAG);
1110 
1111         // Thaw those menu views already open
1112         MenuType[] menuTypes = mMenuTypes;
1113         for (int i = NUM_TYPES - 1; i >= 0; i--) {
1114             if (menuTypes[i] == null) {
1115                 continue;
1116             }
1117 
1118             if (menuTypes[i].hasMenuView()) {
1119                 ((View) menuTypes[i].getMenuView(null)).restoreHierarchyState(viewStates);
1120             }
1121         }
1122     }
1123 
1124     /**
1125      * An adapter that allows an {@link AdapterView} to use this {@link MenuBuilder} as a data
1126      * source.  This adapter will use only the visible/shown items from the menu.
1127      */
1128     public class MenuAdapter extends BaseAdapter {
1129         private int mMenuType;
1130 
MenuAdapter(int menuType)1131         public MenuAdapter(int menuType) {
1132             mMenuType = menuType;
1133         }
1134 
getOffset()1135         public int getOffset() {
1136             if (mMenuType == TYPE_EXPANDED) {
1137                 return getNumIconMenuItemsShown();
1138             } else {
1139                 return 0;
1140             }
1141         }
1142 
getCount()1143         public int getCount() {
1144             return getVisibleItems().size() - getOffset();
1145         }
1146 
getItem(int position)1147         public MenuItemImpl getItem(int position) {
1148             return getVisibleItems().get(position + getOffset());
1149         }
1150 
getItemId(int position)1151         public long getItemId(int position) {
1152             // Since a menu item's ID is optional, we'll use the position as an
1153             // ID for the item in the AdapterView
1154             return position;
1155         }
1156 
getView(int position, View convertView, ViewGroup parent)1157         public View getView(int position, View convertView, ViewGroup parent) {
1158             return ((MenuItemImpl) getItem(position)).getItemView(mMenuType, parent);
1159         }
1160 
1161     }
1162 }
1163