content: /*
   * Copyright (C) 2006 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.actionbarsherlock.internal.view.menu;
  
  
  import java.lang.ref.WeakReference;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.concurrent.CopyOnWriteArrayList;
  
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.content.pm.ResolveInfo;
  import android.content.res.Configuration;
  import android.content.res.Resources;
  import android.graphics.drawable.Drawable;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.Parcelable;
  import android.util.SparseArray;
  import android.view.ContextMenu.ContextMenuInfo;
  import android.view.KeyCharacterMap;
  import android.view.KeyEvent;
  import android.view.View;
  
  import com.actionbarsherlock.R;
  import com.actionbarsherlock.view.ActionProvider;
  import com.actionbarsherlock.view.Menu;
  import com.actionbarsherlock.view.MenuItem;
  import com.actionbarsherlock.view.SubMenu;
  
  /**
   * Implementation of the {@link android.view.Menu} interface for creating a
   * standard menu UI.
   */
  public class MenuBuilder implements Menu {
      //UNUSED private static final String TAG = "MenuBuilder";
  
      private static final String PRESENTER_KEY = "android:menu:presenters";
      private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates";
      private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview";
  
      private static final int[]  sCategoryToOrder = new int[] {
          1, /* No category */
          4, /* CONTAINER */
          5, /* SYSTEM */
          3, /* SECONDARY */
          2, /* ALTERNATIVE */
          0, /* SELECTED_ALTERNATIVE */
      };
  
      private final Context mContext;
      private final Resources mResources;
  
      /**
       * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
       * instead of accessing this directly.
       */
      private boolean mQwertyMode;
  
      /**
       * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
       * instead of accessing this directly.
       */
      private boolean mShortcutsVisible;
  
      /**
       * Callback that will receive the various menu-related events generated by
       * this class. Use getCallback to get a reference to the callback.
       */
      private Callback mCallback;
  
      /** Contains all of the items for this menu */
      private ArrayList<MenuItemImpl> mItems;
  
      /** Contains only the items that are currently visible.  This will be created/refreshed from
       * {@link #getVisibleItems()} */
      private ArrayList<MenuItemImpl> mVisibleItems;
      /**
       * Whether or not the items (or any one item's shown state) has changed since it was last
       * fetched from {@link #getVisibleItems()}
       */
      private boolean mIsVisibleItemsStale;
  
      /**
       * Contains only the items that should appear in the Action Bar, if present.
       */
      private ArrayList<MenuItemImpl> mActionItems;
      /**
       * Contains items that should NOT appear in the Action Bar, if present.
       */
      private ArrayList<MenuItemImpl> mNonActionItems;
  
      /**
       * Whether or not the items (or any one item's action state) has changed since it was
       * last fetched.
       */
      private boolean mIsActionItemsStale;
  
      /**
       * Default value for how added items should show in the action list.
       */
      private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
  
      /**
       * Current use case is Context Menus: As Views populate the context menu, each one has
       * extra information that should be passed along.  This is the current menu info that
       * should be set on all items added to this menu.
       */
      private ContextMenuInfo mCurrentMenuInfo;
  
      /** Header title for menu types that have a header (context and submenus) */
      CharSequence mHeaderTitle;
      /** Header icon for menu types that have a header and support icons (context) */
      Drawable mHeaderIcon;
      /** Header custom view for menu types that have a header and support custom views (context) */
      View mHeaderView;
  
      /**
       * Contains the state of the View hierarchy for all menu views when the menu
       * was frozen.
       */
      //UNUSED private SparseArray<Parcelable> mFrozenViewStates;
  
      /**
       * Prevents onItemsChanged from doing its junk, useful for batching commands
       * that may individually call onItemsChanged.
       */
      private boolean mPreventDispatchingItemsChanged = false;
      private boolean mItemsChangedWhileDispatchPrevented = false;
  
      private boolean mOptionalIconsVisible = false;
  
      private boolean mIsClosing = false;
  
      private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>();
  
      private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
              new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
  
      /**
       * Currently expanded menu item; must be collapsed when we clear.
       */
      private MenuItemImpl mExpandedItem;
  
      /**
       * Called by menu to notify of close and selection changes.
       */
      public interface Callback {
          /**
           * Called when a menu item is selected.
           * @param menu The menu that is the parent of the item
           * @param item The menu item that is selected
           * @return whether the menu item selection was handled
           */
          public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
  
          /**
           * Called when the mode of the menu changes (for example, from icon to expanded).
           *
           * @param menu the menu that has changed modes
           */
          public void onMenuModeChange(MenuBuilder menu);
      }
  
      /**
       * Called by menu items to execute their associated action
       */
      public interface ItemInvoker {
          public boolean invokeItem(MenuItemImpl item);
      }
  
      public MenuBuilder(Context context) {
          mContext = context;
          mResources = context.getResources();
  
          mItems = new ArrayList<MenuItemImpl>();
  
          mVisibleItems = new ArrayList<MenuItemImpl>();
          mIsVisibleItemsStale = true;
  
          mActionItems = new ArrayList<MenuItemImpl>();
          mNonActionItems = new ArrayList<MenuItemImpl>();
          mIsActionItemsStale = true;
  
          setShortcutsVisibleInner(true);
      }
  
      public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) {
          mDefaultShowAsAction = defaultShowAsAction;
          return this;
      }
  
      /**
       * Add a presenter to this menu. This will only hold a WeakReference;
       * you do not need to explicitly remove a presenter, but you can using
       * {@link #removeMenuPresenter(MenuPresenter)}.
       *
       * @param presenter The presenter to add
       */
      public void addMenuPresenter(MenuPresenter presenter) {
          mPresenters.add(new WeakReference<MenuPresenter>(presenter));
          presenter.initForMenu(mContext, this);
          mIsActionItemsStale = true;
      }
  
      /**
       * Remove a presenter from this menu. That presenter will no longer
       * receive notifications of updates to this menu's data.
       *
       * @param presenter The presenter to remove
       */
      public void removeMenuPresenter(MenuPresenter presenter) {
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter item = ref.get();
              if (item == null || item == presenter) {
                  mPresenters.remove(ref);
              }
          }
      }
  
      private void dispatchPresenterUpdate(boolean cleared) {
          if (mPresenters.isEmpty()) return;
  
          stopDispatchingItemsChanged();
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else {
                  presenter.updateMenuView(cleared);
              }
          }
          startDispatchingItemsChanged();
      }
  
      private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) {
          if (mPresenters.isEmpty()) return false;
  
          boolean result = false;
  
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else if (!result) {
                  result = presenter.onSubMenuSelected(subMenu);
              }
          }
          return result;
      }
  
      private void dispatchSaveInstanceState(Bundle outState) {
          if (mPresenters.isEmpty()) return;
  
          SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>();
  
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else {
                  final int id = presenter.getId();
                  if (id > 0) {
                      final Parcelable state = presenter.onSaveInstanceState();
                      if (state != null) {
                          presenterStates.put(id, state);
                      }
                  }
              }
          }
  
          outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates);
      }
  
      private void dispatchRestoreInstanceState(Bundle state) {
          SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY);
  
          if (presenterStates == null || mPresenters.isEmpty()) return;
  
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else {
                  final int id = presenter.getId();
                  if (id > 0) {
                      Parcelable parcel = presenterStates.get(id);
                      if (parcel != null) {
                          presenter.onRestoreInstanceState(parcel);
                      }
                  }
              }
          }
      }
  
      public void savePresenterStates(Bundle outState) {
          dispatchSaveInstanceState(outState);
      }
  
      public void restorePresenterStates(Bundle state) {
          dispatchRestoreInstanceState(state);
      }
  
      public void saveActionViewStates(Bundle outStates) {
          SparseArray<Parcelable> viewStates = null;
  
          final int itemCount = size();
          for (int i = 0; i < itemCount; i++) {
              final MenuItem item = getItem(i);
              final View v = item.getActionView();
              if (v != null && v.getId() != View.NO_ID) {
                  if (viewStates == null) {
                      viewStates = new SparseArray<Parcelable>();
                  }
                  v.saveHierarchyState(viewStates);
                  if (item.isActionViewExpanded()) {
                      outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId());
                  }
              }
              if (item.hasSubMenu()) {
                  final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
                  subMenu.saveActionViewStates(outStates);
              }
          }
  
          if (viewStates != null) {
              outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates);
          }
      }
  
      public void restoreActionViewStates(Bundle states) {
          if (states == null) {
              return;
          }
  
          SparseArray<Parcelable> viewStates = states.getSparseParcelableArray(
                  getActionViewStatesKey());
  
          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && viewStates == null) {
              //Fixes Issue #652 with sdk <= 2.3.6
              return;
          }
  
          final int itemCount = size();
          for (int i = 0; i < itemCount; i++) {
              final MenuItem item = getItem(i);
              final View v = item.getActionView();
              if (v != null && v.getId() != View.NO_ID) {
                  v.restoreHierarchyState(viewStates);
              }
              if (item.hasSubMenu()) {
                  final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
                  subMenu.restoreActionViewStates(states);
              }
          }
  
          final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID);
          if (expandedId > 0) {
              MenuItem itemToExpand = findItem(expandedId);
              if (itemToExpand != null) {
                  itemToExpand.expandActionView();
              }
          }
      }
  
      protected String getActionViewStatesKey() {
          return ACTION_VIEW_STATES_KEY;
      }
  
      public void setCallback(Callback cb) {
          mCallback = cb;
      }
  
      /**
       * Adds an item to the menu.  The other add methods funnel to this.
       */
      private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
          final int ordering = getOrdering(categoryOrder);
  
          final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder,
                  ordering, title, mDefaultShowAsAction);
  
          if (mCurrentMenuInfo != null) {
              // Pass along the current menu info
              item.setMenuInfo(mCurrentMenuInfo);
          }
  
          mItems.add(findInsertIndex(mItems, ordering), item);
          onItemsChanged(true);
  
          return item;
      }
  
      public MenuItem add(CharSequence title) {
          return addInternal(0, 0, 0, title);
      }
  
      public MenuItem add(int titleRes) {
          return addInternal(0, 0, 0, mResources.getString(titleRes));
      }
  
      public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
          return addInternal(group, id, categoryOrder, title);
      }
  
      public MenuItem add(int group, int id, int categoryOrder, int title) {
          return addInternal(group, id, categoryOrder, mResources.getString(title));
      }
  
      public SubMenu addSubMenu(CharSequence title) {
          return addSubMenu(0, 0, 0, title);
      }
  
      public SubMenu addSubMenu(int titleRes) {
          return addSubMenu(0, 0, 0, mResources.getString(titleRes));
      }
  
      public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
          final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
          final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
          item.setSubMenu(subMenu);
  
          return subMenu;
      }
  
      public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
          return addSubMenu(group, id, categoryOrder, mResources.getString(title));
      }
  
      public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
              Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
          PackageManager pm = mContext.getPackageManager();
          final List<ResolveInfo> lri =
                  pm.queryIntentActivityOptions(caller, specifics, intent, 0);
          final int N = lri != null ? lri.size() : 0;
  
          if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
              removeGroup(group);
          }
  
          for (int i=0; i<N; i++) {
              final ResolveInfo ri = lri.get(i);
              Intent rintent = new Intent(
                  ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
              rintent.setComponent(new ComponentName(
                      ri.activityInfo.applicationInfo.packageName,
                      ri.activityInfo.name));
              final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
                      .setIcon(ri.loadIcon(pm))
                      .setIntent(rintent);
              if (outSpecificItems != null && ri.specificIndex >= 0) {
                  outSpecificItems[ri.specificIndex] = item;
              }
          }
  
          return N;
      }
  
      public void removeItem(int id) {
          removeItemAtInt(findItemIndex(id), true);
      }
  
      public void removeGroup(int group) {
          final int i = findGroupIndex(group);
  
          if (i >= 0) {
              final int maxRemovable = mItems.size() - i;
              int numRemoved = 0;
              while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
                  // Don't force update for each one, this method will do it at the end
                  removeItemAtInt(i, false);
              }
  
              // Notify menu views
              onItemsChanged(true);
          }
      }
  
      /**
       * Remove the item at the given index and optionally forces menu views to
       * update.
       *
       * @param index The index of the item to be removed. If this index is
       *            invalid an exception is thrown.
       * @param updateChildrenOnMenuViews Whether to force update on menu views.
       *            Please make sure you eventually call this after your batch of
       *            removals.
       */
      private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
          if ((index < 0) || (index >= mItems.size())) return;
  
          mItems.remove(index);
  
          if (updateChildrenOnMenuViews) onItemsChanged(true);
      }
  
      public void removeItemAt(int index) {
          removeItemAtInt(index, true);
      }
  
      public void clearAll() {
          mPreventDispatchingItemsChanged = true;
          clear();
          clearHeader();
          mPreventDispatchingItemsChanged = false;
          mItemsChangedWhileDispatchPrevented = false;
          onItemsChanged(true);
      }
  
      public void clear() {
          if (mExpandedItem != null) {
              collapseItemActionView(mExpandedItem);
          }
          mItems.clear();
  
          onItemsChanged(true);
      }
  
      void setExclusiveItemChecked(MenuItem item) {
          final int group = item.getGroupId();
  
          final int N = mItems.size();
          for (int i = 0; i < N; i++) {
              MenuItemImpl curItem = mItems.get(i);
              if (curItem.getGroupId() == group) {
                  if (!curItem.isExclusiveCheckable()) continue;
                  if (!curItem.isCheckable()) continue;
  
                  // Check the item meant to be checked, uncheck the others (that are in the group)
                  curItem.setCheckedInt(curItem == item);
              }
          }
      }
  
      public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
          final int N = mItems.size();
  
          for (int i = 0; i < N; i++) {
              MenuItemImpl item = mItems.get(i);
              if (item.getGroupId() == group) {
                  item.setExclusiveCheckable(exclusive);
                  item.setCheckable(checkable);
              }
          }
      }
  
      public void setGroupVisible(int group, boolean visible) {
          final int N = mItems.size();
  
          // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
          // than setVisible and at the end notify of items being changed
  
          boolean changedAtLeastOneItem = false;
          for (int i = 0; i < N; i++) {
              MenuItemImpl item = mItems.get(i);
              if (item.getGroupId() == group) {
                  if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
              }
          }
  
          if (changedAtLeastOneItem) onItemsChanged(true);
      }
  
      public void setGroupEnabled(int group, boolean enabled) {
          final int N = mItems.size();
  
          for (int i = 0; i < N; i++) {
              MenuItemImpl item = mItems.get(i);
              if (item.getGroupId() == group) {
                  item.setEnabled(enabled);
              }
          }
      }
  
      public boolean hasVisibleItems() {
          final int size = size();
  
          for (int i = 0; i < size; i++) {
              MenuItemImpl item = mItems.get(i);
              if (item.isVisible()) {
                  return true;
              }
          }
  
          return false;
      }
  
      public MenuItem findItem(int id) {
          final int size = size();
          for (int i = 0; i < size; i++) {
              MenuItemImpl item = mItems.get(i);
              if (item.getItemId() == id) {
                  return item;
              } else if (item.hasSubMenu()) {
                  MenuItem possibleItem = item.getSubMenu().findItem(id);
  
                  if (possibleItem != null) {
                      return possibleItem;
                  }
              }
          }
  
          return null;
      }
  
      public int findItemIndex(int id) {
          final int size = size();
  
          for (int i = 0; i < size; i++) {
              MenuItemImpl item = mItems.get(i);
              if (item.getItemId() == id) {
                  return i;
              }
          }
  
          return -1;
      }
  
      public int findGroupIndex(int group) {
          return findGroupIndex(group, 0);
      }
  
      public int findGroupIndex(int group, int start) {
          final int size = size();
  
          if (start < 0) {
              start = 0;
          }
  
          for (int i = start; i < size; i++) {
              final MenuItemImpl item = mItems.get(i);
  
              if (item.getGroupId() == group) {
                  return i;
              }
          }
  
          return -1;
      }
  
      public int size() {
          return mItems.size();
      }
  
      /** {@inheritDoc} */
      public MenuItem getItem(int index) {
          return mItems.get(index);
      }
  
      public boolean isShortcutKey(int keyCode, KeyEvent event) {
          return findItemWithShortcutForKey(keyCode, event) != null;
      }
  
      public void setQwertyMode(boolean isQwerty) {
          mQwertyMode = isQwerty;
  
          onItemsChanged(false);
      }
  
      /**
       * Returns the ordering across all items. This will grab the category from
       * the upper bits, find out how to order the category with respect to other
       * categories, and combine it with the lower bits.
       *
       * @param categoryOrder The category order for a particular item (if it has
       *            not been or/add with a category, the default category is
       *            assumed).
       * @return An ordering integer that can be used to order this item across
       *         all the items (even from other categories).
       */
      private static int getOrdering(int categoryOrder) {
          final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
  
          if (index < 0 || index >= sCategoryToOrder.length) {
              throw new IllegalArgumentException("order does not contain a valid category.");
          }
  
          return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
      }
  
      /**
       * @return whether the menu shortcuts are in qwerty mode or not
       */
      boolean isQwertyMode() {
          return mQwertyMode;
      }
  
      /**
       * Sets whether the shortcuts should be visible on menus.  Devices without hardware
       * key input will never make shortcuts visible even if this method is passed 'true'.
       *
       * @param shortcutsVisible Whether shortcuts should be visible (if true and a
       *            menu item does not have a shortcut defined, that item will
       *            still NOT show a shortcut)
       */
      public void setShortcutsVisible(boolean shortcutsVisible) {
          if (mShortcutsVisible == shortcutsVisible) return;
  
          setShortcutsVisibleInner(shortcutsVisible);
          onItemsChanged(false);
      }
  
      private void setShortcutsVisibleInner(boolean shortcutsVisible) {
          mShortcutsVisible = shortcutsVisible
                  && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
                  && mResources.getBoolean(
                          R.bool.abs__config_showMenuShortcutsWhenKeyboardPresent);
      }
  
      /**
       * @return Whether shortcuts should be visible on menus.
       */
      public boolean isShortcutsVisible() {
          return mShortcutsVisible;
      }
  
      Resources getResources() {
          return mResources;
      }
  
      public Context getContext() {
          return mContext;
      }
  
      boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
          return mCallback != null && mCallback.onMenuItemSelected(menu, item);
      }
  
      /**
       * Dispatch a mode change event to this menu's callback.
       */
      public void changeMenuMode() {
          if (mCallback != null) {
              mCallback.onMenuModeChange(this);
          }
      }
  
      private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
          for (int i = items.size() - 1; i >= 0; i--) {
              MenuItemImpl item = items.get(i);
              if (item.getOrdering() <= ordering) {
                  return i + 1;
              }
          }
  
          return 0;
      }
  
      public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
          final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
  
          boolean handled = false;
  
          if (item != null) {
              handled = performItemAction(item, flags);
          }
  
          if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
              close(true);
          }
  
          return handled;
      }
  
      /*
       * This function will return all the menu and sub-menu items that can
       * be directly (the shortcut directly corresponds) and indirectly
       * (the ALT-enabled char corresponds to the shortcut) associated
       * with the keyCode.
       */
      @SuppressWarnings("deprecation")
      void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) {
          final boolean qwerty = isQwertyMode();
          final int metaState = event.getMetaState();
          final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
          // Get the chars associated with the keyCode (i.e using any chording combo)
          final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
          // The delete key is not mapped to '\b' so we treat it specially
          if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
              return;
          }
  
          // Look for an item whose shortcut is this key.
          final int N = mItems.size();
          for (int i = 0; i < N; i++) {
              MenuItemImpl item = mItems.get(i);
              if (item.hasSubMenu()) {
                  ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event);
              }
              final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
              if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) &&
                    (shortcutChar != 0) &&
                    (shortcutChar == possibleChars.meta[0]
                        || shortcutChar == possibleChars.meta[2]
                        || (qwerty && shortcutChar == '\b' &&
                            keyCode == KeyEvent.KEYCODE_DEL)) &&
                    item.isEnabled()) {
                  items.add(item);
              }
          }
      }
  
      /*
       * We want to return the menu item associated with the key, but if there is no
       * ambiguity (i.e. there is only one menu item corresponding to the key) we want
       * to return it even if it's not an exact match; this allow the user to
       * _not_ use the ALT key for example, making the use of shortcuts slightly more
       * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
       * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
       *
       * On the other hand, if two (or more) shortcuts corresponds to the same key,
       * we have to only return the exact match.
       */
      @SuppressWarnings("deprecation")
      MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
          // Get all items that can be associated directly or indirectly with the keyCode
          ArrayList<MenuItemImpl> items = mTempShortcutItemList;
          items.clear();
          findItemsWithShortcutForKey(items, keyCode, event);
  
          if (items.isEmpty()) {
              return null;
          }
  
          final int metaState = event.getMetaState();
          final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
          // Get the chars associated with the keyCode (i.e using any chording combo)
          event.getKeyData(possibleChars);
  
          // If we have only one element, we can safely returns it
          final int size = items.size();
          if (size == 1) {
              return items.get(0);
          }
  
          final boolean qwerty = isQwertyMode();
          // If we found more than one item associated with the key,
          // we have to return the exact match
          for (int i = 0; i < size; i++) {
              final MenuItemImpl item = items.get(i);
              final char shortcutChar = qwerty ? item.getAlphabeticShortcut() :
                      item.getNumericShortcut();
              if ((shortcutChar == possibleChars.meta[0] &&
                      (metaState & KeyEvent.META_ALT_ON) == 0)
                  || (shortcutChar == possibleChars.meta[2] &&
                      (metaState & KeyEvent.META_ALT_ON) != 0)
                  || (qwerty && shortcutChar == '\b' &&
                      keyCode == KeyEvent.KEYCODE_DEL)) {
                  return item;
              }
          }
          return null;
      }
  
      public boolean performIdentifierAction(int id, int flags) {
          // Look for an item whose identifier is the id.
          return performItemAction(findItem(id), flags);
      }
  
      public boolean performItemAction(MenuItem item, int flags) {
          MenuItemImpl itemImpl = (MenuItemImpl) item;
  
          if (itemImpl == null || !itemImpl.isEnabled()) {
              return false;
          }
  
          boolean invoked = itemImpl.invoke();
  
          if (itemImpl.hasCollapsibleActionView()) {
              invoked |= itemImpl.expandActionView();
              if (invoked) close(true);
          } else if (item.hasSubMenu()) {
              close(false);
  
              final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
              final ActionProvider provider = item.getActionProvider();
              if (provider != null && provider.hasSubMenu()) {
                  provider.onPrepareSubMenu(subMenu);
              }
              invoked |= dispatchSubMenuSelected(subMenu);
              if (!invoked) close(true);
          } else {
              if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
                  close(true);
              }
          }
  
          return invoked;
      }
  
      /**
       * Closes the visible menu.
       *
       * @param allMenusAreClosing Whether the menus are completely closing (true),
       *            or whether there is another menu coming in this menu's place
       *            (false). For example, if the menu is closing because a
       *            sub menu is about to be shown, <var>allMenusAreClosing</var>
       *            is false.
       */
      final void close(boolean allMenusAreClosing) {
          if (mIsClosing) return;
  
          mIsClosing = true;
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else {
                  presenter.onCloseMenu(this, allMenusAreClosing);
              }
          }
          mIsClosing = false;
      }
  
      /** {@inheritDoc} */
      public void close() {
          close(true);
      }
  
      /**
       * Called when an item is added or removed.
       *
       * @param structureChanged true if the menu structure changed,
       *                         false if only item properties changed.
       *                         (Visibility is a structural property since it affects layout.)
       */
      void onItemsChanged(boolean structureChanged) {
          if (!mPreventDispatchingItemsChanged) {
              if (structureChanged) {
                  mIsVisibleItemsStale = true;
                  mIsActionItemsStale = true;
              }
  
              dispatchPresenterUpdate(structureChanged);
          } else {
              mItemsChangedWhileDispatchPrevented = true;
          }
      }
  
      /**
       * Stop dispatching item changed events to presenters until
       * {@link #startDispatchingItemsChanged()} is called. Useful when
       * many menu operations are going to be performed as a batch.
       */
      public void stopDispatchingItemsChanged() {
          if (!mPreventDispatchingItemsChanged) {
              mPreventDispatchingItemsChanged = true;
              mItemsChangedWhileDispatchPrevented = false;
          }
      }
  
      public void startDispatchingItemsChanged() {
          mPreventDispatchingItemsChanged = false;
  
          if (mItemsChangedWhileDispatchPrevented) {
              mItemsChangedWhileDispatchPrevented = false;
              onItemsChanged(true);
          }
      }
  
      /**
       * Called by {@link MenuItemImpl} when its visible flag is changed.
       * @param item The item that has gone through a visibility change.
       */
      void onItemVisibleChanged(MenuItemImpl item) {
          // Notify of items being changed
          mIsVisibleItemsStale = true;
          onItemsChanged(true);
      }
  
      /**
       * Called by {@link MenuItemImpl} when its action request status is changed.
       * @param item The item that has gone through a change in action request status.
       */
      void onItemActionRequestChanged(MenuItemImpl item) {
          // Notify of items being changed
          mIsActionItemsStale = true;
          onItemsChanged(true);
      }
  
      ArrayList<MenuItemImpl> getVisibleItems() {
          if (!mIsVisibleItemsStale) return mVisibleItems;
  
          // Refresh the visible items
          mVisibleItems.clear();
  
          final int itemsSize = mItems.size();
          MenuItemImpl item;
          for (int i = 0; i < itemsSize; i++) {
              item = mItems.get(i);
              if (item.isVisible()) mVisibleItems.add(item);
          }
  
          mIsVisibleItemsStale = false;
          mIsActionItemsStale = true;
  
          return mVisibleItems;
      }
  
      /**
       * This method determines which menu items get to be 'action items' that will appear
       * in an action bar and which items should be 'overflow items' in a secondary menu.
       * The rules are as follows:
       *
       * <p>Items are considered for inclusion in the order specified within the menu.
       * There is a limit of mMaxActionItems as a total count, optionally including the overflow
       * menu button itself. This is a soft limit; if an item shares a group ID with an item
       * previously included as an action item, the new item will stay with its group and become
       * an action item itself even if it breaks the max item count limit. This is done to
       * limit the conceptual complexity of the items presented within an action bar. Only a few
       * unrelated concepts should be presented to the user in this space, and groups are treated
       * as a single concept.
       *
       * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This
       * limit may be broken by a single item that exceeds the remaining space, but no further
       * items may be added. If an item that is part of a group cannot fit within the remaining
       * measured width, the entire group will be demoted to overflow. This is done to ensure room
       * for navigation and other affordances in the action bar as well as reduce general UI clutter.
       *
       * <p>The space freed by demoting a full group cannot be consumed by future menu items.
       * Once items begin to overflow, all future items become overflow items as well. This is
       * to avoid inadvertent reordering that may break the app's intended design.
       */
      public void flagActionItems() {
          if (!mIsActionItemsStale) {
              return;
          }
  
          // Presenters flag action items as needed.
          boolean flagged = false;
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else {
                  flagged |= presenter.flagActionItems();
              }
          }
  
          if (flagged) {
              mActionItems.clear();
              mNonActionItems.clear();
              ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
              final int itemsSize = visibleItems.size();
              for (int i = 0; i < itemsSize; i++) {
                  MenuItemImpl item = visibleItems.get(i);
                  if (item.isActionButton()) {
                      mActionItems.add(item);
                  } else {
                      mNonActionItems.add(item);
                  }
              }
          } else {
              // Nobody flagged anything, everything is a non-action item.
              // (This happens during a first pass with no action-item presenters.)
              mActionItems.clear();
              mNonActionItems.clear();
              mNonActionItems.addAll(getVisibleItems());
          }
          mIsActionItemsStale = false;
      }
  
      ArrayList<MenuItemImpl> getActionItems() {
          flagActionItems();
          return mActionItems;
      }
  
      ArrayList<MenuItemImpl> getNonActionItems() {
          flagActionItems();
          return mNonActionItems;
      }
  
      public void clearHeader() {
          mHeaderIcon = null;
          mHeaderTitle = null;
          mHeaderView = null;
  
          onItemsChanged(false);
      }
  
      private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
              final Drawable icon, final View view) {
          final Resources r = getResources();
  
          if (view != null) {
              mHeaderView = view;
  
              // If using a custom view, then the title and icon aren't used
              mHeaderTitle = null;
              mHeaderIcon = null;
          } else {
              if (titleRes > 0) {
                  mHeaderTitle = r.getText(titleRes);
              } else if (title != null) {
                  mHeaderTitle = title;
              }
  
              if (iconRes > 0) {
                  mHeaderIcon = r.getDrawable(iconRes);
              } else if (icon != null) {
                  mHeaderIcon = icon;
              }
  
              // If using the title or icon, then a custom view isn't used
              mHeaderView = null;
          }
  
          // Notify of change
          onItemsChanged(false);
      }
  
      /**
       * Sets the header's title. This replaces the header view. Called by the
       * builder-style methods of subclasses.
       *
       * @param title The new title.
       * @return This MenuBuilder so additional setters can be called.
       */
      protected MenuBuilder setHeaderTitleInt(CharSequence title) {
          setHeaderInternal(0, title, 0, null, null);
          return this;
      }
  
      /**
       * Sets the header's title. This replaces the header view. Called by the
       * builder-style methods of subclasses.
       *
       * @param titleRes The new title (as a resource ID).
       * @return This MenuBuilder so additional setters can be called.
       */
      protected MenuBuilder setHeaderTitleInt(int titleRes) {
          setHeaderInternal(titleRes, null, 0, null, null);
          return this;
      }
  
      /**
       * Sets the header's icon. This replaces the header view. Called by the
       * builder-style methods of subclasses.
       *
       * @param icon The new icon.
       * @return This MenuBuilder so additional setters can be called.
       */
      protected MenuBuilder setHeaderIconInt(Drawable icon) {
          setHeaderInternal(0, null, 0, icon, null);
          return this;
      }
  
      /**
       * Sets the header's icon. This replaces the header view. Called by the
       * builder-style methods of subclasses.
       *
       * @param iconRes The new icon (as a resource ID).
       * @return This MenuBuilder so additional setters can be called.
       */
      protected MenuBuilder setHeaderIconInt(int iconRes) {
          setHeaderInternal(0, null, iconRes, null, null);
          return this;
      }
  
      /**
       * Sets the header's view. This replaces the title and icon. Called by the
       * builder-style methods of subclasses.
       *
       * @param view The new view.
       * @return This MenuBuilder so additional setters can be called.
       */
      protected MenuBuilder setHeaderViewInt(View view) {
          setHeaderInternal(0, null, 0, null, view);
          return this;
      }
  
      public CharSequence getHeaderTitle() {
          return mHeaderTitle;
      }
  
      public Drawable getHeaderIcon() {
          return mHeaderIcon;
      }
  
      public View getHeaderView() {
          return mHeaderView;
      }
  
      /**
       * Gets the root menu (if this is a submenu, find its root menu).
       * @return The root menu.
       */
      public MenuBuilder getRootMenu() {
          return this;
      }
  
      /**
       * Sets the current menu info that is set on all items added to this menu
       * (until this is called again with different menu info, in which case that
       * one will be added to all subsequent item additions).
       *
       * @param menuInfo The extra menu information to add.
       */
      public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
          mCurrentMenuInfo = menuInfo;
      }
  
      void setOptionalIconsVisible(boolean visible) {
          mOptionalIconsVisible = visible;
      }
  
      boolean getOptionalIconsVisible() {
          return mOptionalIconsVisible;
      }
  
      public boolean expandItemActionView(MenuItemImpl item) {
          if (mPresenters.isEmpty()) return false;
  
          boolean expanded = false;
  
          stopDispatchingItemsChanged();
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else if ((expanded = presenter.expandItemActionView(this, item))) {
                  break;
              }
          }
          startDispatchingItemsChanged();
  
          if (expanded) {
              mExpandedItem = item;
          }
          return expanded;
      }
  
      public boolean collapseItemActionView(MenuItemImpl item) {
          if (mPresenters.isEmpty() || mExpandedItem != item) return false;
  
          boolean collapsed = false;
  
          stopDispatchingItemsChanged();
          for (WeakReference<MenuPresenter> ref : mPresenters) {
              final MenuPresenter presenter = ref.get();
              if (presenter == null) {
                  mPresenters.remove(ref);
              } else if ((collapsed = presenter.collapseItemActionView(this, item))) {
                  break;
              }
          }
          startDispatchingItemsChanged();
  
          if (collapsed) {
              mExpandedItem = null;
          }
          return collapsed;
      }
  
      public MenuItemImpl getExpandedItem() {
          return mExpandedItem;
      }
  
      public boolean bindNativeOverflow(android.view.Menu menu, android.view.MenuItem.OnMenuItemClickListener listener, HashMap<android.view.MenuItem, MenuItemImpl> map) {
          final List<MenuItemImpl> nonActionItems = getNonActionItems();
          if (nonActionItems == null || nonActionItems.size() == 0) {
              return false;
          }
  
          boolean visible = false;
          menu.clear();
          for (MenuItemImpl nonActionItem : nonActionItems) {
              if (!nonActionItem.isVisible()) {
                  continue;
              }
              visible = true;
  
              android.view.MenuItem nativeItem;
              if (nonActionItem.hasSubMenu()) {
                  android.view.SubMenu nativeSub = menu.addSubMenu(nonActionItem.getGroupId(), nonActionItem.getItemId(),
                          nonActionItem.getOrder(), nonActionItem.getTitle());
  
                  SubMenuBuilder subMenu = (SubMenuBuilder)nonActionItem.getSubMenu();
                  for (MenuItemImpl subItem : subMenu.getVisibleItems()) {
                      android.view.MenuItem nativeSubItem = nativeSub.add(subItem.getGroupId(), subItem.getItemId(),
                              subItem.getOrder(), subItem.getTitle());
  
                      nativeSubItem.setIcon(subItem.getIcon());
                      nativeSubItem.setOnMenuItemClickListener(listener);
                      nativeSubItem.setEnabled(subItem.isEnabled());
                      nativeSubItem.setIntent(subItem.getIntent());
                      nativeSubItem.setNumericShortcut(subItem.getNumericShortcut());
                      nativeSubItem.setAlphabeticShortcut(subItem.getAlphabeticShortcut());
                      nativeSubItem.setTitleCondensed(subItem.getTitleCondensed());
                      nativeSubItem.setCheckable(subItem.isCheckable());
                      nativeSubItem.setChecked(subItem.isChecked());
  
                      if (subItem.isExclusiveCheckable()) {
                          nativeSub.setGroupCheckable(subItem.getGroupId(), true, true);
                      }
  
                      map.put(nativeSubItem, subItem);
                  }
  
                  nativeItem = nativeSub.getItem();
              } else {
                  nativeItem = menu.add(nonActionItem.getGroupId(), nonActionItem.getItemId(),
                          nonActionItem.getOrder(), nonActionItem.getTitle());
              }
              nativeItem.setIcon(nonActionItem.getIcon());
              nativeItem.setOnMenuItemClickListener(listener);
              nativeItem.setEnabled(nonActionItem.isEnabled());
              nativeItem.setIntent(nonActionItem.getIntent());
              nativeItem.setNumericShortcut(nonActionItem.getNumericShortcut());
              nativeItem.setAlphabeticShortcut(nonActionItem.getAlphabeticShortcut());
              nativeItem.setTitleCondensed(nonActionItem.getTitleCondensed());
              nativeItem.setCheckable(nonActionItem.isCheckable());
              nativeItem.setChecked(nonActionItem.isChecked());
  
              if (nonActionItem.isExclusiveCheckable()) {
                  menu.setGroupCheckable(nonActionItem.getGroupId(), true, true);
              }
  
              map.put(nativeItem, nonActionItem);
          }
          return visible;
      }
  }
  