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