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