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