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.content.ComponentName;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.pm.PackageManager;
     24 import android.content.pm.ResolveInfo;
     25 import android.content.res.Configuration;
     26 import android.content.res.Resources;
     27 import android.graphics.drawable.Drawable;
     28 import android.os.Bundle;
     29 import android.os.Parcelable;
     30 import android.util.SparseArray;
     31 import android.view.ActionProvider;
     32 import android.view.ContextMenu.ContextMenuInfo;
     33 import android.view.KeyCharacterMap;
     34 import android.view.KeyEvent;
     35 import android.view.Menu;
     36 import android.view.MenuItem;
     37 import android.view.SubMenu;
     38 import android.view.View;
     39 
     40 import java.lang.ref.WeakReference;
     41 import java.util.ArrayList;
     42 import java.util.List;
     43 import java.util.concurrent.CopyOnWriteArrayList;
     44 
     45 /**
     46  * Implementation of the {@link android.view.Menu} interface for creating a
     47  * standard menu UI.
     48  */
     49 public class MenuBuilder implements Menu {
     50     private static final String TAG = "MenuBuilder";
     51 
     52     private static final String PRESENTER_KEY = "android:menu:presenters";
     53     private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates";
     54     private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview";
     55 
     56     private static final int[]  sCategoryToOrder = new int[] {
     57         1, /* No category */
     58         4, /* CONTAINER */
     59         5, /* SYSTEM */
     60         3, /* SECONDARY */
     61         2, /* ALTERNATIVE */
     62         0, /* SELECTED_ALTERNATIVE */
     63     };
     64 
     65     private final Context mContext;
     66     private final Resources mResources;
     67 
     68     /**
     69      * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
     70      * instead of accessing this directly.
     71      */
     72     private boolean mQwertyMode;
     73 
     74     /**
     75      * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
     76      * instead of accessing this directly.
     77      */
     78     private boolean mShortcutsVisible;
     79 
     80     /**
     81      * Callback that will receive the various menu-related events generated by
     82      * this class. Use getCallback to get a reference to the callback.
     83      */
     84     private Callback mCallback;
     85 
     86     /** Contains all of the items for this menu */
     87     private ArrayList<MenuItemImpl> mItems;
     88 
     89     /** Contains only the items that are currently visible.  This will be created/refreshed from
     90      * {@link #getVisibleItems()} */
     91     private ArrayList<MenuItemImpl> mVisibleItems;
     92     /**
     93      * Whether or not the items (or any one item's shown state) has changed since it was last
     94      * fetched from {@link #getVisibleItems()}
     95      */
     96     private boolean mIsVisibleItemsStale;
     97 
     98     /**
     99      * Contains only the items that should appear in the Action Bar, if present.
    100      */
    101     private ArrayList<MenuItemImpl> mActionItems;
    102     /**
    103      * Contains items that should NOT appear in the Action Bar, if present.
    104      */
    105     private ArrayList<MenuItemImpl> mNonActionItems;
    106 
    107     /**
    108      * Whether or not the items (or any one item's action state) has changed since it was
    109      * last fetched.
    110      */
    111     private boolean mIsActionItemsStale;
    112 
    113     /**
    114      * Default value for how added items should show in the action list.
    115      */
    116     private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
    117 
    118     /**
    119      * Current use case is Context Menus: As Views populate the context menu, each one has
    120      * extra information that should be passed along.  This is the current menu info that
    121      * should be set on all items added to this menu.
    122      */
    123     private ContextMenuInfo mCurrentMenuInfo;
    124 
    125     /** Header title for menu types that have a header (context and submenus) */
    126     CharSequence mHeaderTitle;
    127     /** Header icon for menu types that have a header and support icons (context) */
    128     Drawable mHeaderIcon;
    129     /** Header custom view for menu types that have a header and support custom views (context) */
    130     View mHeaderView;
    131 
    132     /**
    133      * Contains the state of the View hierarchy for all menu views when the menu
    134      * was frozen.
    135      */
    136     private SparseArray<Parcelable> mFrozenViewStates;
    137 
    138     /**
    139      * Prevents onItemsChanged from doing its junk, useful for batching commands
    140      * that may individually call onItemsChanged.
    141      */
    142     private boolean mPreventDispatchingItemsChanged = false;
    143     private boolean mItemsChangedWhileDispatchPrevented = false;
    144 
    145     private boolean mOptionalIconsVisible = false;
    146 
    147     private boolean mIsClosing = false;
    148 
    149     private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>();
    150 
    151     private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
    152             new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
    153 
    154     /**
    155      * Currently expanded menu item; must be collapsed when we clear.
    156      */
    157     private MenuItemImpl mExpandedItem;
    158 
    159     /**
    160      * Called by menu to notify of close and selection changes.
    161      */
    162     public interface Callback {
    163         /**
    164          * Called when a menu item is selected.
    165          * @param menu The menu that is the parent of the item
    166          * @param item The menu item that is selected
    167          * @return whether the menu item selection was handled
    168          */
    169         public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
    170 
    171         /**
    172          * Called when the mode of the menu changes (for example, from icon to expanded).
    173          *
    174          * @param menu the menu that has changed modes
    175          */
    176         public void onMenuModeChange(MenuBuilder menu);
    177     }
    178 
    179     /**
    180      * Called by menu items to execute their associated action
    181      */
    182     public interface ItemInvoker {
    183         public boolean invokeItem(MenuItemImpl item);
    184     }
    185 
    186     public MenuBuilder(Context context) {
    187         mContext = context;
    188         mResources = context.getResources();
    189 
    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);
    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) close(true);
    911         } else if (itemImpl.hasSubMenu() || providerHasSubMenu) {
    912             close(false);
    913 
    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) close(true);
    924         } else {
    925             if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
    926                 close(true);
    927             }
    928         }
    929 
    930         return invoked;
    931     }
    932 
    933     /**
    934      * Closes the visible menu.
    935      *
    936      * @param allMenusAreClosing Whether the menus are completely closing (true),
    937      *            or whether there is another menu coming in this menu's place
    938      *            (false). For example, if the menu is closing because a
    939      *            sub menu is about to be shown, <var>allMenusAreClosing</var>
    940      *            is false.
    941      */
    942     public final void close(boolean allMenusAreClosing) {
    943         if (mIsClosing) return;
    944 
    945         mIsClosing = true;
    946         for (WeakReference<MenuPresenter> ref : mPresenters) {
    947             final MenuPresenter presenter = ref.get();
    948             if (presenter == null) {
    949                 mPresenters.remove(ref);
    950             } else {
    951                 presenter.onCloseMenu(this, allMenusAreClosing);
    952             }
    953         }
    954         mIsClosing = false;
    955     }
    956 
    957     /** {@inheritDoc} */
    958     public void close() {
    959         close(true);
    960     }
    961 
    962     /**
    963      * Called when an item is added or removed.
    964      *
    965      * @param structureChanged true if the menu structure changed,
    966      *                         false if only item properties changed.
    967      *                         (Visibility is a structural property since it affects layout.)
    968      */
    969     public void onItemsChanged(boolean structureChanged) {
    970         if (!mPreventDispatchingItemsChanged) {
    971             if (structureChanged) {
    972                 mIsVisibleItemsStale = true;
    973                 mIsActionItemsStale = true;
    974             }
    975 
    976             dispatchPresenterUpdate(structureChanged);
    977         } else {
    978             mItemsChangedWhileDispatchPrevented = true;
    979         }
    980     }
    981 
    982     /**
    983      * Stop dispatching item changed events to presenters until
    984      * {@link #startDispatchingItemsChanged()} is called. Useful when
    985      * many menu operations are going to be performed as a batch.
    986      */
    987     public void stopDispatchingItemsChanged() {
    988         if (!mPreventDispatchingItemsChanged) {
    989             mPreventDispatchingItemsChanged = true;
    990             mItemsChangedWhileDispatchPrevented = false;
    991         }
    992     }
    993 
    994     public void startDispatchingItemsChanged() {
    995         mPreventDispatchingItemsChanged = false;
    996 
    997         if (mItemsChangedWhileDispatchPrevented) {
    998             mItemsChangedWhileDispatchPrevented = false;
    999             onItemsChanged(true);
   1000         }
   1001     }
   1002 
   1003     /**
   1004      * Called by {@link MenuItemImpl} when its visible flag is changed.
   1005      * @param item The item that has gone through a visibility change.
   1006      */
   1007     void onItemVisibleChanged(MenuItemImpl item) {
   1008         // Notify of items being changed
   1009         mIsVisibleItemsStale = true;
   1010         onItemsChanged(true);
   1011     }
   1012 
   1013     /**
   1014      * Called by {@link MenuItemImpl} when its action request status is changed.
   1015      * @param item The item that has gone through a change in action request status.
   1016      */
   1017     void onItemActionRequestChanged(MenuItemImpl item) {
   1018         // Notify of items being changed
   1019         mIsActionItemsStale = true;
   1020         onItemsChanged(true);
   1021     }
   1022 
   1023     public ArrayList<MenuItemImpl> getVisibleItems() {
   1024         if (!mIsVisibleItemsStale) return mVisibleItems;
   1025 
   1026         // Refresh the visible items
   1027         mVisibleItems.clear();
   1028 
   1029         final int itemsSize = mItems.size();
   1030         MenuItemImpl item;
   1031         for (int i = 0; i < itemsSize; i++) {
   1032             item = mItems.get(i);
   1033             if (item.isVisible()) mVisibleItems.add(item);
   1034         }
   1035 
   1036         mIsVisibleItemsStale = false;
   1037         mIsActionItemsStale = true;
   1038 
   1039         return mVisibleItems;
   1040     }
   1041 
   1042     /**
   1043      * This method determines which menu items get to be 'action items' that will appear
   1044      * in an action bar and which items should be 'overflow items' in a secondary menu.
   1045      * The rules are as follows:
   1046      *
   1047      * <p>Items are considered for inclusion in the order specified within the menu.
   1048      * There is a limit of mMaxActionItems as a total count, optionally including the overflow
   1049      * menu button itself. This is a soft limit; if an item shares a group ID with an item
   1050      * previously included as an action item, the new item will stay with its group and become
   1051      * an action item itself even if it breaks the max item count limit. This is done to
   1052      * limit the conceptual complexity of the items presented within an action bar. Only a few
   1053      * unrelated concepts should be presented to the user in this space, and groups are treated
   1054      * as a single concept.
   1055      *
   1056      * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This
   1057      * limit may be broken by a single item that exceeds the remaining space, but no further
   1058      * items may be added. If an item that is part of a group cannot fit within the remaining
   1059      * measured width, the entire group will be demoted to overflow. This is done to ensure room
   1060      * for navigation and other affordances in the action bar as well as reduce general UI clutter.
   1061      *
   1062      * <p>The space freed by demoting a full group cannot be consumed by future menu items.
   1063      * Once items begin to overflow, all future items become overflow items as well. This is
   1064      * to avoid inadvertent reordering that may break the app's intended design.
   1065      */
   1066     public void flagActionItems() {
   1067         // Important side effect: if getVisibleItems is stale it may refresh,
   1068         // which can affect action items staleness.
   1069         final ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
   1070 
   1071         if (!mIsActionItemsStale) {
   1072             return;
   1073         }
   1074 
   1075         // Presenters flag action items as needed.
   1076         boolean flagged = false;
   1077         for (WeakReference<MenuPresenter> ref : mPresenters) {
   1078             final MenuPresenter presenter = ref.get();
   1079             if (presenter == null) {
   1080                 mPresenters.remove(ref);
   1081             } else {
   1082                 flagged |= presenter.flagActionItems();
   1083             }
   1084         }
   1085 
   1086         if (flagged) {
   1087             mActionItems.clear();
   1088             mNonActionItems.clear();
   1089             final int itemsSize = visibleItems.size();
   1090             for (int i = 0; i < itemsSize; i++) {
   1091                 MenuItemImpl item = visibleItems.get(i);
   1092                 if (item.isActionButton()) {
   1093                     mActionItems.add(item);
   1094                 } else {
   1095                     mNonActionItems.add(item);
   1096                 }
   1097             }
   1098         } else {
   1099             // Nobody flagged anything, everything is a non-action item.
   1100             // (This happens during a first pass with no action-item presenters.)
   1101             mActionItems.clear();
   1102             mNonActionItems.clear();
   1103             mNonActionItems.addAll(getVisibleItems());
   1104         }
   1105         mIsActionItemsStale = false;
   1106     }
   1107 
   1108     public ArrayList<MenuItemImpl> getActionItems() {
   1109         flagActionItems();
   1110         return mActionItems;
   1111     }
   1112 
   1113     public ArrayList<MenuItemImpl> getNonActionItems() {
   1114         flagActionItems();
   1115         return mNonActionItems;
   1116     }
   1117 
   1118     public void clearHeader() {
   1119         mHeaderIcon = null;
   1120         mHeaderTitle = null;
   1121         mHeaderView = null;
   1122 
   1123         onItemsChanged(false);
   1124     }
   1125 
   1126     private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
   1127             final Drawable icon, final View view) {
   1128         final Resources r = getResources();
   1129 
   1130         if (view != null) {
   1131             mHeaderView = view;
   1132 
   1133             // If using a custom view, then the title and icon aren't used
   1134             mHeaderTitle = null;
   1135             mHeaderIcon = null;
   1136         } else {
   1137             if (titleRes > 0) {
   1138                 mHeaderTitle = r.getText(titleRes);
   1139             } else if (title != null) {
   1140                 mHeaderTitle = title;
   1141             }
   1142 
   1143             if (iconRes > 0) {
   1144                 mHeaderIcon = getContext().getDrawable(iconRes);
   1145             } else if (icon != null) {
   1146                 mHeaderIcon = icon;
   1147             }
   1148 
   1149             // If using the title or icon, then a custom view isn't used
   1150             mHeaderView = null;
   1151         }
   1152 
   1153         // Notify of change
   1154         onItemsChanged(false);
   1155     }
   1156 
   1157     /**
   1158      * Sets the header's title. This replaces the header view. Called by the
   1159      * builder-style methods of subclasses.
   1160      *
   1161      * @param title The new title.
   1162      * @return This MenuBuilder so additional setters can be called.
   1163      */
   1164     protected MenuBuilder setHeaderTitleInt(CharSequence title) {
   1165         setHeaderInternal(0, title, 0, null, null);
   1166         return this;
   1167     }
   1168 
   1169     /**
   1170      * Sets the header's title. This replaces the header view. Called by the
   1171      * builder-style methods of subclasses.
   1172      *
   1173      * @param titleRes The new title (as a resource ID).
   1174      * @return This MenuBuilder so additional setters can be called.
   1175      */
   1176     protected MenuBuilder setHeaderTitleInt(int titleRes) {
   1177         setHeaderInternal(titleRes, null, 0, null, null);
   1178         return this;
   1179     }
   1180 
   1181     /**
   1182      * Sets the header's icon. This replaces the header view. Called by the
   1183      * builder-style methods of subclasses.
   1184      *
   1185      * @param icon The new icon.
   1186      * @return This MenuBuilder so additional setters can be called.
   1187      */
   1188     protected MenuBuilder setHeaderIconInt(Drawable icon) {
   1189         setHeaderInternal(0, null, 0, icon, null);
   1190         return this;
   1191     }
   1192 
   1193     /**
   1194      * Sets the header's icon. This replaces the header view. Called by the
   1195      * builder-style methods of subclasses.
   1196      *
   1197      * @param iconRes The new icon (as a resource ID).
   1198      * @return This MenuBuilder so additional setters can be called.
   1199      */
   1200     protected MenuBuilder setHeaderIconInt(int iconRes) {
   1201         setHeaderInternal(0, null, iconRes, null, null);
   1202         return this;
   1203     }
   1204 
   1205     /**
   1206      * Sets the header's view. This replaces the title and icon. Called by the
   1207      * builder-style methods of subclasses.
   1208      *
   1209      * @param view The new view.
   1210      * @return This MenuBuilder so additional setters can be called.
   1211      */
   1212     protected MenuBuilder setHeaderViewInt(View view) {
   1213         setHeaderInternal(0, null, 0, null, view);
   1214         return this;
   1215     }
   1216 
   1217     public CharSequence getHeaderTitle() {
   1218         return mHeaderTitle;
   1219     }
   1220 
   1221     public Drawable getHeaderIcon() {
   1222         return mHeaderIcon;
   1223     }
   1224 
   1225     public View getHeaderView() {
   1226         return mHeaderView;
   1227     }
   1228 
   1229     /**
   1230      * Gets the root menu (if this is a submenu, find its root menu).
   1231      * @return The root menu.
   1232      */
   1233     public MenuBuilder getRootMenu() {
   1234         return this;
   1235     }
   1236 
   1237     /**
   1238      * Sets the current menu info that is set on all items added to this menu
   1239      * (until this is called again with different menu info, in which case that
   1240      * one will be added to all subsequent item additions).
   1241      *
   1242      * @param menuInfo The extra menu information to add.
   1243      */
   1244     public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
   1245         mCurrentMenuInfo = menuInfo;
   1246     }
   1247 
   1248     void setOptionalIconsVisible(boolean visible) {
   1249         mOptionalIconsVisible = visible;
   1250     }
   1251 
   1252     boolean getOptionalIconsVisible() {
   1253         return mOptionalIconsVisible;
   1254     }
   1255 
   1256     public boolean expandItemActionView(MenuItemImpl item) {
   1257         if (mPresenters.isEmpty()) return false;
   1258 
   1259         boolean expanded = false;
   1260 
   1261         stopDispatchingItemsChanged();
   1262         for (WeakReference<MenuPresenter> ref : mPresenters) {
   1263             final MenuPresenter presenter = ref.get();
   1264             if (presenter == null) {
   1265                 mPresenters.remove(ref);
   1266             } else if ((expanded = presenter.expandItemActionView(this, item))) {
   1267                 break;
   1268             }
   1269         }
   1270         startDispatchingItemsChanged();
   1271 
   1272         if (expanded) {
   1273             mExpandedItem = item;
   1274         }
   1275         return expanded;
   1276     }
   1277 
   1278     public boolean collapseItemActionView(MenuItemImpl item) {
   1279         if (mPresenters.isEmpty() || mExpandedItem != item) return false;
   1280 
   1281         boolean collapsed = false;
   1282 
   1283         stopDispatchingItemsChanged();
   1284         for (WeakReference<MenuPresenter> ref : mPresenters) {
   1285             final MenuPresenter presenter = ref.get();
   1286             if (presenter == null) {
   1287                 mPresenters.remove(ref);
   1288             } else if ((collapsed = presenter.collapseItemActionView(this, item))) {
   1289                 break;
   1290             }
   1291         }
   1292         startDispatchingItemsChanged();
   1293 
   1294         if (collapsed) {
   1295             mExpandedItem = null;
   1296         }
   1297         return collapsed;
   1298     }
   1299 
   1300     public MenuItemImpl getExpandedItem() {
   1301         return mExpandedItem;
   1302     }
   1303 }
   1304