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