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