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