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