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