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