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