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