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.ContextThemeWrapper;
     32 import android.view.KeyCharacterMap;
     33 import android.view.KeyEvent;
     34 import android.view.Menu;
     35 import android.view.MenuItem;
     36 import android.view.SubMenu;
     37 import android.view.View;
     38 import android.view.ViewGroup;
     39 import android.view.LayoutInflater;
     40 import android.view.ContextMenu.ContextMenuInfo;
     41 import android.widget.AdapterView;
     42 import android.widget.BaseAdapter;
     43 
     44 import java.lang.ref.WeakReference;
     45 import java.util.ArrayList;
     46 import java.util.List;
     47 import java.util.Vector;
     48 
     49 /**
     50  * Implementation of the {@link android.view.Menu} interface for creating a
     51  * standard menu UI.
     52  */
     53 public class MenuBuilder implements Menu {
     54     private static final String LOGTAG = "MenuBuilder";
     55 
     56     /** The number of different menu types */
     57     public static final int NUM_TYPES = 3;
     58     /** The menu type that represents the icon menu view */
     59     public static final int TYPE_ICON = 0;
     60     /** The menu type that represents the expanded menu view */
     61     public static final int TYPE_EXPANDED = 1;
     62     /**
     63      * The menu type that represents a menu dialog. Examples are context and sub
     64      * menus. This menu type will not have a corresponding MenuView, but it will
     65      * have an ItemView.
     66      */
     67     public static final int TYPE_DIALOG = 2;
     68 
     69     private static final String VIEWS_TAG = "android:views";
     70 
     71     // Order must be the same order as the TYPE_*
     72     static final int THEME_RES_FOR_TYPE[] = new int[] {
     73         com.android.internal.R.style.Theme_IconMenu,
     74         com.android.internal.R.style.Theme_ExpandedMenu,
     75         0,
     76     };
     77 
     78     // Order must be the same order as the TYPE_*
     79     static final int LAYOUT_RES_FOR_TYPE[] = new int[] {
     80         com.android.internal.R.layout.icon_menu_layout,
     81         com.android.internal.R.layout.expanded_menu_layout,
     82         0,
     83     };
     84 
     85     // Order must be the same order as the TYPE_*
     86     static final int ITEM_LAYOUT_RES_FOR_TYPE[] = new int[] {
     87         com.android.internal.R.layout.icon_menu_item_layout,
     88         com.android.internal.R.layout.list_menu_item_layout,
     89         com.android.internal.R.layout.list_menu_item_layout,
     90     };
     91 
     92     private static final int[]  sCategoryToOrder = new int[] {
     93         1, /* No category */
     94         4, /* CONTAINER */
     95         5, /* SYSTEM */
     96         3, /* SECONDARY */
     97         2, /* ALTERNATIVE */
     98         0, /* SELECTED_ALTERNATIVE */
     99     };
    100 
    101     private final Context mContext;
    102     private final Resources mResources;
    103 
    104     /**
    105      * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
    106      * instead of accessing this directly.
    107      */
    108     private boolean mQwertyMode;
    109 
    110     /**
    111      * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
    112      * instead of accessing this directly.
    113      */
    114     private boolean mShortcutsVisible;
    115 
    116     /**
    117      * Callback that will receive the various menu-related events generated by
    118      * this class. Use getCallback to get a reference to the callback.
    119      */
    120     private Callback mCallback;
    121 
    122     /** Contains all of the items for this menu */
    123     private ArrayList<MenuItemImpl> mItems;
    124 
    125     /** Contains only the items that are currently visible.  This will be created/refreshed from
    126      * {@link #getVisibleItems()} */
    127     private ArrayList<MenuItemImpl> mVisibleItems;
    128     /**
    129      * Whether or not the items (or any one item's shown state) has changed since it was last
    130      * fetched from {@link #getVisibleItems()}
    131      */
    132     private boolean mIsVisibleItemsStale;
    133 
    134     /**
    135      * Current use case is Context Menus: As Views populate the context menu, each one has
    136      * extra information that should be passed along.  This is the current menu info that
    137      * should be set on all items added to this menu.
    138      */
    139     private ContextMenuInfo mCurrentMenuInfo;
    140 
    141     /** Header title for menu types that have a header (context and submenus) */
    142     CharSequence mHeaderTitle;
    143     /** Header icon for menu types that have a header and support icons (context) */
    144     Drawable mHeaderIcon;
    145     /** Header custom view for menu types that have a header and support custom views (context) */
    146     View mHeaderView;
    147 
    148     /**
    149      * Contains the state of the View hierarchy for all menu views when the menu
    150      * was frozen.
    151      */
    152     private SparseArray<Parcelable> mFrozenViewStates;
    153 
    154     /**
    155      * Prevents onItemsChanged from doing its junk, useful for batching commands
    156      * that may individually call onItemsChanged.
    157      */
    158     private boolean mPreventDispatchingItemsChanged = false;
    159 
    160     private boolean mOptionalIconsVisible = false;
    161 
    162     private MenuType[] mMenuTypes;
    163     class MenuType {
    164         private int mMenuType;
    165 
    166         /** The layout inflater that uses the menu type's theme */
    167         private LayoutInflater mInflater;
    168 
    169         /** The lazily loaded {@link MenuView} */
    170         private WeakReference<MenuView> mMenuView;
    171 
    172         MenuType(int menuType) {
    173             mMenuType = menuType;
    174         }
    175 
    176         LayoutInflater getInflater() {
    177             // Create an inflater that uses the given theme for the Views it inflates
    178             if (mInflater == null) {
    179                 Context wrappedContext = new ContextThemeWrapper(mContext,
    180                         THEME_RES_FOR_TYPE[mMenuType]);
    181                 mInflater = (LayoutInflater) wrappedContext
    182                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    183             }
    184 
    185             return mInflater;
    186         }
    187 
    188         MenuView getMenuView(ViewGroup parent) {
    189             if (LAYOUT_RES_FOR_TYPE[mMenuType] == 0) {
    190                 return null;
    191             }
    192 
    193             synchronized (this) {
    194                 MenuView menuView = mMenuView != null ? mMenuView.get() : null;
    195 
    196                 if (menuView == null) {
    197                     menuView = (MenuView) getInflater().inflate(
    198                             LAYOUT_RES_FOR_TYPE[mMenuType], parent, false);
    199                     menuView.initialize(MenuBuilder.this, mMenuType);
    200 
    201                     // Cache the view
    202                     mMenuView = new WeakReference<MenuView>(menuView);
    203 
    204                     if (mFrozenViewStates != null) {
    205                         View view = (View) menuView;
    206                         view.restoreHierarchyState(mFrozenViewStates);
    207 
    208                         // Clear this menu type's frozen state, since we just restored it
    209                         mFrozenViewStates.remove(view.getId());
    210                     }
    211                 }
    212 
    213                 return menuView;
    214             }
    215         }
    216 
    217         boolean hasMenuView() {
    218             return mMenuView != null && mMenuView.get() != null;
    219         }
    220     }
    221 
    222     /**
    223      * Called by menu to notify of close and selection changes
    224      */
    225     public interface Callback {
    226         /**
    227          * Called when a menu item is selected.
    228          * @param menu The menu that is the parent of the item
    229          * @param item The menu item that is selected
    230          * @return whether the menu item selection was handled
    231          */
    232         public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
    233 
    234         /**
    235          * Called when a menu is closed.
    236          * @param menu The menu that was closed.
    237          * @param allMenusAreClosing Whether the menus are completely closing (true),
    238          *            or whether there is another menu opening shortly
    239          *            (false). For example, if the menu is closing because a
    240          *            sub menu is about to be shown, <var>allMenusAreClosing</var>
    241          *            is false.
    242          */
    243         public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing);
    244 
    245         /**
    246          * Called when a sub menu is selected.  This is a cue to open the given sub menu's decor.
    247          * @param subMenu the sub menu that is being opened
    248          * @return whether the sub menu selection was handled by the callback
    249          */
    250         public boolean onSubMenuSelected(SubMenuBuilder subMenu);
    251 
    252         /**
    253          * Called when a sub menu is closed
    254          * @param menu the sub menu that was closed
    255          */
    256         public void onCloseSubMenu(SubMenuBuilder menu);
    257 
    258         /**
    259          * Called when the mode of the menu changes (for example, from icon to expanded).
    260          *
    261          * @param menu the menu that has changed modes
    262          */
    263         public void onMenuModeChange(MenuBuilder menu);
    264     }
    265 
    266     /**
    267      * Called by menu items to execute their associated action
    268      */
    269     public interface ItemInvoker {
    270         public boolean invokeItem(MenuItemImpl item);
    271     }
    272 
    273     public MenuBuilder(Context context) {
    274         mMenuTypes = new MenuType[NUM_TYPES];
    275 
    276         mContext = context;
    277         mResources = context.getResources();
    278 
    279         mItems = new ArrayList<MenuItemImpl>();
    280 
    281         mVisibleItems = new ArrayList<MenuItemImpl>();
    282         mIsVisibleItemsStale = true;
    283 
    284         mShortcutsVisible =
    285                 (mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS);
    286     }
    287 
    288     public void setCallback(Callback callback) {
    289         mCallback = callback;
    290     }
    291 
    292     MenuType getMenuType(int menuType) {
    293         if (mMenuTypes[menuType] == null) {
    294             mMenuTypes[menuType] = new MenuType(menuType);
    295         }
    296 
    297         return mMenuTypes[menuType];
    298     }
    299 
    300     /**
    301      * Gets a menu View that contains this menu's items.
    302      *
    303      * @param menuType The type of menu to get a View for (must be one of
    304      *            {@link #TYPE_ICON}, {@link #TYPE_EXPANDED},
    305      *            {@link #TYPE_DIALOG}).
    306      * @param parent The ViewGroup that provides a set of LayoutParams values
    307      *            for this menu view
    308      * @return A View for the menu of type <var>menuType</var>
    309      */
    310     public View getMenuView(int menuType, ViewGroup parent) {
    311         // The expanded menu depends on the number if items shown in the icon menu (which
    312         // is adjustable as setters/XML attributes on IconMenuView [imagine a larger LCD
    313         // wanting to show more icons]). If, for example, the activity goes through
    314         // an orientation change while the expanded menu is open, the icon menu's view
    315         // won't have an instance anymore; so here we make sure we have an icon menu view (matching
    316         // the same parent so the layout parameters from the XML are used). This
    317         // will create the icon menu view and cache it (if it doesn't already exist).
    318         if (menuType == TYPE_EXPANDED
    319                 && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) {
    320             getMenuType(TYPE_ICON).getMenuView(parent);
    321         }
    322 
    323         return (View) getMenuType(menuType).getMenuView(parent);
    324     }
    325 
    326     private int getNumIconMenuItemsShown() {
    327         ViewGroup parent = null;
    328 
    329         if (!mMenuTypes[TYPE_ICON].hasMenuView()) {
    330             /*
    331              * There isn't an icon menu view instantiated, so when we get it
    332              * below, it will lazily instantiate it. We should pass a proper
    333              * parent so it uses the layout_ attributes present in the XML
    334              * layout file.
    335              */
    336             if (mMenuTypes[TYPE_EXPANDED].hasMenuView()) {
    337                 View expandedMenuView = (View) mMenuTypes[TYPE_EXPANDED].getMenuView(null);
    338                 parent = (ViewGroup) expandedMenuView.getParent();
    339             }
    340         }
    341 
    342         return ((IconMenuView) getMenuView(TYPE_ICON, parent)).getNumActualItemsShown();
    343     }
    344 
    345     /**
    346      * Clears the cached menu views. Call this if the menu views need to another
    347      * layout (for example, if the screen size has changed).
    348      */
    349     public void clearMenuViews() {
    350         for (int i = NUM_TYPES - 1; i >= 0; i--) {
    351             if (mMenuTypes[i] != null) {
    352                 mMenuTypes[i].mMenuView = null;
    353             }
    354         }
    355 
    356         for (int i = mItems.size() - 1; i >= 0; i--) {
    357             MenuItemImpl item = mItems.get(i);
    358             if (item.hasSubMenu()) {
    359                 ((SubMenuBuilder) item.getSubMenu()).clearMenuViews();
    360             }
    361             item.clearItemViews();
    362         }
    363     }
    364 
    365     /**
    366      * Adds an item to the menu.  The other add methods funnel to this.
    367      */
    368     private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
    369         final int ordering = getOrdering(categoryOrder);
    370 
    371         final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder, ordering, title);
    372 
    373         if (mCurrentMenuInfo != null) {
    374             // Pass along the current menu info
    375             item.setMenuInfo(mCurrentMenuInfo);
    376         }
    377 
    378         mItems.add(findInsertIndex(mItems, ordering), item);
    379         onItemsChanged(false);
    380 
    381         return item;
    382     }
    383 
    384     public MenuItem add(CharSequence title) {
    385         return addInternal(0, 0, 0, title);
    386     }
    387 
    388     public MenuItem add(int titleRes) {
    389         return addInternal(0, 0, 0, mResources.getString(titleRes));
    390     }
    391 
    392     public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
    393         return addInternal(group, id, categoryOrder, title);
    394     }
    395 
    396     public MenuItem add(int group, int id, int categoryOrder, int title) {
    397         return addInternal(group, id, categoryOrder, mResources.getString(title));
    398     }
    399 
    400     public SubMenu addSubMenu(CharSequence title) {
    401         return addSubMenu(0, 0, 0, title);
    402     }
    403 
    404     public SubMenu addSubMenu(int titleRes) {
    405         return addSubMenu(0, 0, 0, mResources.getString(titleRes));
    406     }
    407 
    408     public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
    409         final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
    410         final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
    411         item.setSubMenu(subMenu);
    412 
    413         return subMenu;
    414     }
    415 
    416     public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
    417         return addSubMenu(group, id, categoryOrder, mResources.getString(title));
    418     }
    419 
    420     public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
    421             Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
    422         PackageManager pm = mContext.getPackageManager();
    423         final List<ResolveInfo> lri =
    424                 pm.queryIntentActivityOptions(caller, specifics, intent, 0);
    425         final int N = lri != null ? lri.size() : 0;
    426 
    427         if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
    428             removeGroup(group);
    429         }
    430 
    431         for (int i=0; i<N; i++) {
    432             final ResolveInfo ri = lri.get(i);
    433             Intent rintent = new Intent(
    434                 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
    435             rintent.setComponent(new ComponentName(
    436                     ri.activityInfo.applicationInfo.packageName,
    437                     ri.activityInfo.name));
    438             final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
    439                     .setIcon(ri.loadIcon(pm))
    440                     .setIntent(rintent);
    441             if (outSpecificItems != null && ri.specificIndex >= 0) {
    442                 outSpecificItems[ri.specificIndex] = item;
    443             }
    444         }
    445 
    446         return N;
    447     }
    448 
    449     public void removeItem(int id) {
    450         removeItemAtInt(findItemIndex(id), true);
    451     }
    452 
    453     public void removeGroup(int group) {
    454         final int i = findGroupIndex(group);
    455 
    456         if (i >= 0) {
    457             final int maxRemovable = mItems.size() - i;
    458             int numRemoved = 0;
    459             while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
    460                 // Don't force update for each one, this method will do it at the end
    461                 removeItemAtInt(i, false);
    462             }
    463 
    464             // Notify menu views
    465             onItemsChanged(false);
    466         }
    467     }
    468 
    469     /**
    470      * Remove the item at the given index and optionally forces menu views to
    471      * update.
    472      *
    473      * @param index The index of the item to be removed. If this index is
    474      *            invalid an exception is thrown.
    475      * @param updateChildrenOnMenuViews Whether to force update on menu views.
    476      *            Please make sure you eventually call this after your batch of
    477      *            removals.
    478      */
    479     private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
    480         if ((index < 0) || (index >= mItems.size())) return;
    481 
    482         mItems.remove(index);
    483 
    484         if (updateChildrenOnMenuViews) onItemsChanged(false);
    485     }
    486 
    487     public void removeItemAt(int index) {
    488         removeItemAtInt(index, true);
    489     }
    490 
    491     public void clearAll() {
    492         mPreventDispatchingItemsChanged = true;
    493         clear();
    494         clearHeader();
    495         mPreventDispatchingItemsChanged = false;
    496         onItemsChanged(true);
    497     }
    498 
    499     public void clear() {
    500         mItems.clear();
    501 
    502         onItemsChanged(true);
    503     }
    504 
    505     void setExclusiveItemChecked(MenuItem item) {
    506         final int group = item.getGroupId();
    507 
    508         final int N = mItems.size();
    509         for (int i = 0; i < N; i++) {
    510             MenuItemImpl curItem = mItems.get(i);
    511             if (curItem.getGroupId() == group) {
    512                 if (!curItem.isExclusiveCheckable()) continue;
    513                 if (!curItem.isCheckable()) continue;
    514 
    515                 // Check the item meant to be checked, uncheck the others (that are in the group)
    516                 curItem.setCheckedInt(curItem == item);
    517             }
    518         }
    519     }
    520 
    521     public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
    522         final int N = mItems.size();
    523 
    524         for (int i = 0; i < N; i++) {
    525             MenuItemImpl item = mItems.get(i);
    526             if (item.getGroupId() == group) {
    527                 item.setExclusiveCheckable(exclusive);
    528                 item.setCheckable(checkable);
    529             }
    530         }
    531     }
    532 
    533     public void setGroupVisible(int group, boolean visible) {
    534         final int N = mItems.size();
    535 
    536         // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
    537         // than setVisible and at the end notify of items being changed
    538 
    539         boolean changedAtLeastOneItem = false;
    540         for (int i = 0; i < N; i++) {
    541             MenuItemImpl item = mItems.get(i);
    542             if (item.getGroupId() == group) {
    543                 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
    544             }
    545         }
    546 
    547         if (changedAtLeastOneItem) onItemsChanged(false);
    548     }
    549 
    550     public void setGroupEnabled(int group, boolean enabled) {
    551         final int N = mItems.size();
    552 
    553         for (int i = 0; i < N; i++) {
    554             MenuItemImpl item = mItems.get(i);
    555             if (item.getGroupId() == group) {
    556                 item.setEnabled(enabled);
    557             }
    558         }
    559     }
    560 
    561     public boolean hasVisibleItems() {
    562         final int size = size();
    563 
    564         for (int i = 0; i < size; i++) {
    565             MenuItemImpl item = mItems.get(i);
    566             if (item.isVisible()) {
    567                 return true;
    568             }
    569         }
    570 
    571         return false;
    572     }
    573 
    574     public MenuItem findItem(int id) {
    575         final int size = size();
    576         for (int i = 0; i < size; i++) {
    577             MenuItemImpl item = mItems.get(i);
    578             if (item.getItemId() == id) {
    579                 return item;
    580             } else if (item.hasSubMenu()) {
    581                 MenuItem possibleItem = item.getSubMenu().findItem(id);
    582 
    583                 if (possibleItem != null) {
    584                     return possibleItem;
    585                 }
    586             }
    587         }
    588 
    589         return null;
    590     }
    591 
    592     public int findItemIndex(int id) {
    593         final int size = size();
    594 
    595         for (int i = 0; i < size; i++) {
    596             MenuItemImpl item = mItems.get(i);
    597             if (item.getItemId() == id) {
    598                 return i;
    599             }
    600         }
    601 
    602         return -1;
    603     }
    604 
    605     public int findGroupIndex(int group) {
    606         return findGroupIndex(group, 0);
    607     }
    608 
    609     public int findGroupIndex(int group, int start) {
    610         final int size = size();
    611 
    612         if (start < 0) {
    613             start = 0;
    614         }
    615 
    616         for (int i = start; i < size; i++) {
    617             final MenuItemImpl item = mItems.get(i);
    618 
    619             if (item.getGroupId() == group) {
    620                 return i;
    621             }
    622         }
    623 
    624         return -1;
    625     }
    626 
    627     public int size() {
    628         return mItems.size();
    629     }
    630 
    631     /** {@inheritDoc} */
    632     public MenuItem getItem(int index) {
    633         return mItems.get(index);
    634     }
    635 
    636     public boolean isShortcutKey(int keyCode, KeyEvent event) {
    637         return findItemWithShortcutForKey(keyCode, event) != null;
    638     }
    639 
    640     public void setQwertyMode(boolean isQwerty) {
    641         mQwertyMode = isQwerty;
    642 
    643         refreshShortcuts(isShortcutsVisible(), isQwerty);
    644     }
    645 
    646     /**
    647      * Returns the ordering across all items. This will grab the category from
    648      * the upper bits, find out how to order the category with respect to other
    649      * categories, and combine it with the lower bits.
    650      *
    651      * @param categoryOrder The category order for a particular item (if it has
    652      *            not been or/add with a category, the default category is
    653      *            assumed).
    654      * @return An ordering integer that can be used to order this item across
    655      *         all the items (even from other categories).
    656      */
    657     private static int getOrdering(int categoryOrder)
    658     {
    659         final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
    660 
    661         if (index < 0 || index >= sCategoryToOrder.length) {
    662             throw new IllegalArgumentException("order does not contain a valid category.");
    663         }
    664 
    665         return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
    666     }
    667 
    668     /**
    669      * @return whether the menu shortcuts are in qwerty mode or not
    670      */
    671     boolean isQwertyMode() {
    672         return mQwertyMode;
    673     }
    674 
    675     /**
    676      * Refreshes the shortcut labels on each of the displayed items.  Passes the arguments
    677      * so submenus don't need to call their parent menu for the same values.
    678      */
    679     private void refreshShortcuts(boolean shortcutsVisible, boolean qwertyMode) {
    680         MenuItemImpl item;
    681         for (int i = mItems.size() - 1; i >= 0; i--) {
    682             item = mItems.get(i);
    683 
    684             if (item.hasSubMenu()) {
    685                 ((MenuBuilder) item.getSubMenu()).refreshShortcuts(shortcutsVisible, qwertyMode);
    686             }
    687 
    688             item.refreshShortcutOnItemViews(shortcutsVisible, qwertyMode);
    689         }
    690     }
    691 
    692     /**
    693      * Sets whether the shortcuts should be visible on menus.  Devices without hardware
    694      * key input will never make shortcuts visible even if this method is passed 'true'.
    695      *
    696      * @param shortcutsVisible Whether shortcuts should be visible (if true and a
    697      *            menu item does not have a shortcut defined, that item will
    698      *            still NOT show a shortcut)
    699      */
    700     public void setShortcutsVisible(boolean shortcutsVisible) {
    701         if (mShortcutsVisible == shortcutsVisible) return;
    702 
    703         mShortcutsVisible =
    704             (mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS)
    705             && shortcutsVisible;
    706 
    707         refreshShortcuts(mShortcutsVisible, isQwertyMode());
    708     }
    709 
    710     /**
    711      * @return Whether shortcuts should be visible on menus.
    712      */
    713     public boolean isShortcutsVisible() {
    714         return mShortcutsVisible;
    715     }
    716 
    717     Resources getResources() {
    718         return mResources;
    719     }
    720 
    721     public Callback getCallback() {
    722         return mCallback;
    723     }
    724 
    725     public Context getContext() {
    726         return mContext;
    727     }
    728 
    729     private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
    730         for (int i = items.size() - 1; i >= 0; i--) {
    731             MenuItemImpl item = items.get(i);
    732             if (item.getOrdering() <= ordering) {
    733                 return i + 1;
    734             }
    735         }
    736 
    737         return 0;
    738     }
    739 
    740     public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
    741         final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
    742 
    743         boolean handled = false;
    744 
    745         if (item != null) {
    746             handled = performItemAction(item, flags);
    747         }
    748 
    749         if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
    750             close(true);
    751         }
    752 
    753         return handled;
    754     }
    755 
    756     /*
    757      * This function will return all the menu and sub-menu items that can
    758      * be directly (the shortcut directly corresponds) and indirectly
    759      * (the ALT-enabled char corresponds to the shortcut) associated
    760      * with the keyCode.
    761      */
    762     List<MenuItemImpl> findItemsWithShortcutForKey(int keyCode, KeyEvent event) {
    763         final boolean qwerty = isQwertyMode();
    764         final int metaState = event.getMetaState();
    765         final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
    766         // Get the chars associated with the keyCode (i.e using any chording combo)
    767         final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
    768         // The delete key is not mapped to '\b' so we treat it specially
    769         if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
    770             return null;
    771         }
    772 
    773         Vector<MenuItemImpl> items = new Vector();
    774         // Look for an item whose shortcut is this key.
    775         final int N = mItems.size();
    776         for (int i = 0; i < N; i++) {
    777             MenuItemImpl item = mItems.get(i);
    778             if (item.hasSubMenu()) {
    779                 List<MenuItemImpl> subMenuItems = ((MenuBuilder)item.getSubMenu())
    780                     .findItemsWithShortcutForKey(keyCode, event);
    781                 items.addAll(subMenuItems);
    782             }
    783             final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
    784             if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) &&
    785                   (shortcutChar != 0) &&
    786                   (shortcutChar == possibleChars.meta[0]
    787                       || shortcutChar == possibleChars.meta[2]
    788                       || (qwerty && shortcutChar == '\b' &&
    789                           keyCode == KeyEvent.KEYCODE_DEL)) &&
    790                   item.isEnabled()) {
    791                 items.add(item);
    792             }
    793         }
    794         return items;
    795     }
    796 
    797     /*
    798      * We want to return the menu item associated with the key, but if there is no
    799      * ambiguity (i.e. there is only one menu item corresponding to the key) we want
    800      * to return it even if it's not an exact match; this allow the user to
    801      * _not_ use the ALT key for example, making the use of shortcuts slightly more
    802      * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
    803      * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
    804      *
    805      * On the other hand, if two (or more) shortcuts corresponds to the same key,
    806      * we have to only return the exact match.
    807      */
    808     MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
    809         // Get all items that can be associated directly or indirectly with the keyCode
    810         List<MenuItemImpl> items = findItemsWithShortcutForKey(keyCode, event);
    811 
    812         if (items == null) {
    813             return null;
    814         }
    815 
    816         final int metaState = event.getMetaState();
    817         final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
    818         // Get the chars associated with the keyCode (i.e using any chording combo)
    819         event.getKeyData(possibleChars);
    820 
    821         // If we have only one element, we can safely returns it
    822         if (items.size() == 1) {
    823             return items.get(0);
    824         }
    825 
    826         final boolean qwerty = isQwertyMode();
    827         // If we found more than one item associated with the key,
    828         // we have to return the exact match
    829         for (MenuItemImpl item : items) {
    830             final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
    831             if ((shortcutChar == possibleChars.meta[0] &&
    832                     (metaState & KeyEvent.META_ALT_ON) == 0)
    833                 || (shortcutChar == possibleChars.meta[2] &&
    834                     (metaState & KeyEvent.META_ALT_ON) != 0)
    835                 || (qwerty && shortcutChar == '\b' &&
    836                     keyCode == KeyEvent.KEYCODE_DEL)) {
    837                 return item;
    838             }
    839         }
    840         return null;
    841     }
    842 
    843     public boolean performIdentifierAction(int id, int flags) {
    844         // Look for an item whose identifier is the id.
    845         return performItemAction(findItem(id), flags);
    846     }
    847 
    848     public boolean performItemAction(MenuItem item, int flags) {
    849         MenuItemImpl itemImpl = (MenuItemImpl) item;
    850 
    851         if (itemImpl == null || !itemImpl.isEnabled()) {
    852             return false;
    853         }
    854 
    855         boolean invoked = itemImpl.invoke();
    856 
    857         if (item.hasSubMenu()) {
    858             close(false);
    859 
    860             if (mCallback != null) {
    861                 // Return true if the sub menu was invoked or the item was invoked previously
    862                 invoked = mCallback.onSubMenuSelected((SubMenuBuilder) item.getSubMenu())
    863                         || invoked;
    864             }
    865         } else {
    866             if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
    867                 close(true);
    868             }
    869         }
    870 
    871         return invoked;
    872     }
    873 
    874     /**
    875      * Closes the visible menu.
    876      *
    877      * @param allMenusAreClosing Whether the menus are completely closing (true),
    878      *            or whether there is another menu coming in this menu's place
    879      *            (false). For example, if the menu is closing because a
    880      *            sub menu is about to be shown, <var>allMenusAreClosing</var>
    881      *            is false.
    882      */
    883     final void close(boolean allMenusAreClosing) {
    884         Callback callback = getCallback();
    885         if (callback != null) {
    886             callback.onCloseMenu(this, allMenusAreClosing);
    887         }
    888     }
    889 
    890     /** {@inheritDoc} */
    891     public void close() {
    892         close(true);
    893     }
    894 
    895     /**
    896      * Called when an item is added or removed.
    897      *
    898      * @param cleared Whether the items were cleared or just changed.
    899      */
    900     private void onItemsChanged(boolean cleared) {
    901         if (!mPreventDispatchingItemsChanged) {
    902             if (mIsVisibleItemsStale == false) mIsVisibleItemsStale = true;
    903 
    904             MenuType[] menuTypes = mMenuTypes;
    905             for (int i = 0; i < NUM_TYPES; i++) {
    906                 if ((menuTypes[i] != null) && (menuTypes[i].hasMenuView())) {
    907                     MenuView menuView = menuTypes[i].mMenuView.get();
    908                     menuView.updateChildren(cleared);
    909                 }
    910             }
    911         }
    912     }
    913 
    914     /**
    915      * Called by {@link MenuItemImpl} when its visible flag is changed.
    916      * @param item The item that has gone through a visibility change.
    917      */
    918     void onItemVisibleChanged(MenuItemImpl item) {
    919         // Notify of items being changed
    920         onItemsChanged(false);
    921     }
    922 
    923     ArrayList<MenuItemImpl> getVisibleItems() {
    924         if (!mIsVisibleItemsStale) return mVisibleItems;
    925 
    926         // Refresh the visible items
    927         mVisibleItems.clear();
    928 
    929         final int itemsSize = mItems.size();
    930         MenuItemImpl item;
    931         for (int i = 0; i < itemsSize; i++) {
    932             item = mItems.get(i);
    933             if (item.isVisible()) mVisibleItems.add(item);
    934         }
    935 
    936         mIsVisibleItemsStale = false;
    937 
    938         return mVisibleItems;
    939     }
    940 
    941     public void clearHeader() {
    942         mHeaderIcon = null;
    943         mHeaderTitle = null;
    944         mHeaderView = null;
    945 
    946         onItemsChanged(false);
    947     }
    948 
    949     private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
    950             final Drawable icon, final View view) {
    951         final Resources r = getResources();
    952 
    953         if (view != null) {
    954             mHeaderView = view;
    955 
    956             // If using a custom view, then the title and icon aren't used
    957             mHeaderTitle = null;
    958             mHeaderIcon = null;
    959         } else {
    960             if (titleRes > 0) {
    961                 mHeaderTitle = r.getText(titleRes);
    962             } else if (title != null) {
    963                 mHeaderTitle = title;
    964             }
    965 
    966             if (iconRes > 0) {
    967                 mHeaderIcon = r.getDrawable(iconRes);
    968             } else if (icon != null) {
    969                 mHeaderIcon = icon;
    970             }
    971 
    972             // If using the title or icon, then a custom view isn't used
    973             mHeaderView = null;
    974         }
    975 
    976         // Notify of change
    977         onItemsChanged(false);
    978     }
    979 
    980     /**
    981      * Sets the header's title. This replaces the header view. Called by the
    982      * builder-style methods of subclasses.
    983      *
    984      * @param title The new title.
    985      * @return This MenuBuilder so additional setters can be called.
    986      */
    987     protected MenuBuilder setHeaderTitleInt(CharSequence title) {
    988         setHeaderInternal(0, title, 0, null, null);
    989         return this;
    990     }
    991 
    992     /**
    993      * Sets the header's title. This replaces the header view. Called by the
    994      * builder-style methods of subclasses.
    995      *
    996      * @param titleRes The new title (as a resource ID).
    997      * @return This MenuBuilder so additional setters can be called.
    998      */
    999     protected MenuBuilder setHeaderTitleInt(int titleRes) {
   1000         setHeaderInternal(titleRes, null, 0, null, null);
   1001         return this;
   1002     }
   1003 
   1004     /**
   1005      * Sets the header's icon. This replaces the header view. Called by the
   1006      * builder-style methods of subclasses.
   1007      *
   1008      * @param icon The new icon.
   1009      * @return This MenuBuilder so additional setters can be called.
   1010      */
   1011     protected MenuBuilder setHeaderIconInt(Drawable icon) {
   1012         setHeaderInternal(0, null, 0, icon, null);
   1013         return this;
   1014     }
   1015 
   1016     /**
   1017      * Sets the header's icon. This replaces the header view. Called by the
   1018      * builder-style methods of subclasses.
   1019      *
   1020      * @param iconRes The new icon (as a resource ID).
   1021      * @return This MenuBuilder so additional setters can be called.
   1022      */
   1023     protected MenuBuilder setHeaderIconInt(int iconRes) {
   1024         setHeaderInternal(0, null, iconRes, null, null);
   1025         return this;
   1026     }
   1027 
   1028     /**
   1029      * Sets the header's view. This replaces the title and icon. Called by the
   1030      * builder-style methods of subclasses.
   1031      *
   1032      * @param view The new view.
   1033      * @return This MenuBuilder so additional setters can be called.
   1034      */
   1035     protected MenuBuilder setHeaderViewInt(View view) {
   1036         setHeaderInternal(0, null, 0, null, view);
   1037         return this;
   1038     }
   1039 
   1040     public CharSequence getHeaderTitle() {
   1041         return mHeaderTitle;
   1042     }
   1043 
   1044     public Drawable getHeaderIcon() {
   1045         return mHeaderIcon;
   1046     }
   1047 
   1048     public View getHeaderView() {
   1049         return mHeaderView;
   1050     }
   1051 
   1052     /**
   1053      * Gets the root menu (if this is a submenu, find its root menu).
   1054      * @return The root menu.
   1055      */
   1056     public MenuBuilder getRootMenu() {
   1057         return this;
   1058     }
   1059 
   1060     /**
   1061      * Sets the current menu info that is set on all items added to this menu
   1062      * (until this is called again with different menu info, in which case that
   1063      * one will be added to all subsequent item additions).
   1064      *
   1065      * @param menuInfo The extra menu information to add.
   1066      */
   1067     public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
   1068         mCurrentMenuInfo = menuInfo;
   1069     }
   1070 
   1071     /**
   1072      * Gets an adapter for providing items and their views.
   1073      *
   1074      * @param menuType The type of menu to get an adapter for.
   1075      * @return A {@link MenuAdapter} for this menu with the given menu type.
   1076      */
   1077     public MenuAdapter getMenuAdapter(int menuType) {
   1078         return new MenuAdapter(menuType);
   1079     }
   1080 
   1081     void setOptionalIconsVisible(boolean visible) {
   1082         mOptionalIconsVisible = visible;
   1083     }
   1084 
   1085     boolean getOptionalIconsVisible() {
   1086         return mOptionalIconsVisible;
   1087     }
   1088 
   1089     public void saveHierarchyState(Bundle outState) {
   1090         SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>();
   1091 
   1092         MenuType[] menuTypes = mMenuTypes;
   1093         for (int i = NUM_TYPES - 1; i >= 0; i--) {
   1094             if (menuTypes[i] == null) {
   1095                 continue;
   1096             }
   1097 
   1098             if (menuTypes[i].hasMenuView()) {
   1099                 ((View) menuTypes[i].getMenuView(null)).saveHierarchyState(viewStates);
   1100             }
   1101         }
   1102 
   1103         outState.putSparseParcelableArray(VIEWS_TAG, viewStates);
   1104     }
   1105 
   1106     public void restoreHierarchyState(Bundle inState) {
   1107         // Save this for menu views opened later
   1108         SparseArray<Parcelable> viewStates = mFrozenViewStates = inState
   1109                 .getSparseParcelableArray(VIEWS_TAG);
   1110 
   1111         // Thaw those menu views already open
   1112         MenuType[] menuTypes = mMenuTypes;
   1113         for (int i = NUM_TYPES - 1; i >= 0; i--) {
   1114             if (menuTypes[i] == null) {
   1115                 continue;
   1116             }
   1117 
   1118             if (menuTypes[i].hasMenuView()) {
   1119                 ((View) menuTypes[i].getMenuView(null)).restoreHierarchyState(viewStates);
   1120             }
   1121         }
   1122     }
   1123 
   1124     /**
   1125      * An adapter that allows an {@link AdapterView} to use this {@link MenuBuilder} as a data
   1126      * source.  This adapter will use only the visible/shown items from the menu.
   1127      */
   1128     public class MenuAdapter extends BaseAdapter {
   1129         private int mMenuType;
   1130 
   1131         public MenuAdapter(int menuType) {
   1132             mMenuType = menuType;
   1133         }
   1134 
   1135         public int getOffset() {
   1136             if (mMenuType == TYPE_EXPANDED) {
   1137                 return getNumIconMenuItemsShown();
   1138             } else {
   1139                 return 0;
   1140             }
   1141         }
   1142 
   1143         public int getCount() {
   1144             return getVisibleItems().size() - getOffset();
   1145         }
   1146 
   1147         public MenuItemImpl getItem(int position) {
   1148             return getVisibleItems().get(position + getOffset());
   1149         }
   1150 
   1151         public long getItemId(int position) {
   1152             // Since a menu item's ID is optional, we'll use the position as an
   1153             // ID for the item in the AdapterView
   1154             return position;
   1155         }
   1156 
   1157         public View getView(int position, View convertView, ViewGroup parent) {
   1158             return ((MenuItemImpl) getItem(position)).getItemView(mMenuType, parent);
   1159         }
   1160 
   1161     }
   1162 }
   1163