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