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