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