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