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