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