/* * 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 android.view; import com.android.internal.view.menu.MenuItemImpl; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.annotation.MenuRes; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; /** * This class is used to instantiate menu XML files into Menu objects. *

* For performance reasons, menu inflation relies heavily on pre-processing of * XML files that is done at build time. Therefore, it is not currently possible * to use MenuInflater with an XmlPullParser over a plain XML file at runtime; * it only works with an XmlPullParser returned from a compiled resource (R. * something file.) */ public class MenuInflater { private static final String LOG_TAG = "MenuInflater"; /** Menu tag name in XML. */ private static final String XML_MENU = "menu"; /** Group tag name in XML. */ private static final String XML_GROUP = "group"; /** Item tag name in XML. */ private static final String XML_ITEM = "item"; private static final int NO_ID = 0; private static final Class[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class}; private static final Class[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = ACTION_VIEW_CONSTRUCTOR_SIGNATURE; private final Object[] mActionViewConstructorArguments; private final Object[] mActionProviderConstructorArguments; private Context mContext; private Object mRealOwner; /** * Constructs a menu inflater. * * @see Activity#getMenuInflater() */ public MenuInflater(Context context) { mContext = context; mActionViewConstructorArguments = new Object[] {context}; mActionProviderConstructorArguments = mActionViewConstructorArguments; } /** * Constructs a menu inflater. * * @see Activity#getMenuInflater() * @hide */ public MenuInflater(Context context, Object realOwner) { mContext = context; mRealOwner = realOwner; mActionViewConstructorArguments = new Object[] {context}; mActionProviderConstructorArguments = mActionViewConstructorArguments; } /** * Inflate a menu hierarchy from the specified XML resource. Throws * {@link InflateException} if there is an error. * * @param menuRes Resource ID for an XML layout resource to load (e.g., * R.menu.main_activity) * @param menu The Menu to inflate into. The items and submenus will be * added to this Menu. */ public void inflate(@MenuRes int menuRes, Menu menu) { XmlResourceParser parser = null; try { parser = mContext.getResources().getLayout(menuRes); AttributeSet attrs = Xml.asAttributeSet(parser); parseMenu(parser, attrs, menu); } catch (XmlPullParserException e) { throw new InflateException("Error inflating menu XML", e); } catch (IOException e) { throw new InflateException("Error inflating menu XML", e); } finally { if (parser != null) parser.close(); } } /** * Called internally to fill the given menu. If a sub menu is seen, it will * call this recursively. */ private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) throws XmlPullParserException, IOException { MenuState menuState = new MenuState(menu); int eventType = parser.getEventType(); String tagName; boolean lookingForEndOfUnknownTag = false; String unknownTagName = null; // This loop will skip to the menu start tag do { if (eventType == XmlPullParser.START_TAG) { tagName = parser.getName(); if (tagName.equals(XML_MENU)) { // Go to next tag eventType = parser.next(); break; } throw new RuntimeException("Expecting menu, got " + tagName); } eventType = parser.next(); } while (eventType != XmlPullParser.END_DOCUMENT); boolean reachedEndOfMenu = false; while (!reachedEndOfMenu) { switch (eventType) { case XmlPullParser.START_TAG: if (lookingForEndOfUnknownTag) { break; } tagName = parser.getName(); if (tagName.equals(XML_GROUP)) { menuState.readGroup(attrs); } else if (tagName.equals(XML_ITEM)) { menuState.readItem(attrs); } else if (tagName.equals(XML_MENU)) { // A menu start tag denotes a submenu for an item SubMenu subMenu = menuState.addSubMenuItem(); registerMenu(subMenu, attrs); // Parse the submenu into returned SubMenu parseMenu(parser, attrs, subMenu); } else { lookingForEndOfUnknownTag = true; unknownTagName = tagName; } break; case XmlPullParser.END_TAG: tagName = parser.getName(); if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { lookingForEndOfUnknownTag = false; unknownTagName = null; } else if (tagName.equals(XML_GROUP)) { menuState.resetGroup(); } else if (tagName.equals(XML_ITEM)) { // Add the item if it hasn't been added (if the item was // a submenu, it would have been added already) if (!menuState.hasAddedItem()) { if (menuState.itemActionProvider != null && menuState.itemActionProvider.hasSubMenu()) { registerMenu(menuState.addSubMenuItem(), attrs); } else { registerMenu(menuState.addItem(), attrs); } } } else if (tagName.equals(XML_MENU)) { reachedEndOfMenu = true; } break; case XmlPullParser.END_DOCUMENT: throw new RuntimeException("Unexpected end of document"); } eventType = parser.next(); } } /** * The method is a hook for layoutlib to do its magic. * Nothing is needed outside of LayoutLib. However, it should not be deleted because it * appears to do nothing. */ private void registerMenu(@SuppressWarnings("unused") MenuItem item, @SuppressWarnings("unused") AttributeSet set) { } /** * The method is a hook for layoutlib to do its magic. * Nothing is needed outside of LayoutLib. However, it should not be deleted because it * appears to do nothing. */ private void registerMenu(@SuppressWarnings("unused") SubMenu subMenu, @SuppressWarnings("unused") AttributeSet set) { } // Needed by layoutlib. /*package*/ Context getContext() { return mContext; } private static class InflatedOnMenuItemClickListener implements MenuItem.OnMenuItemClickListener { private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class }; private Object mRealOwner; private Method mMethod; public InflatedOnMenuItemClickListener(Object realOwner, String methodName) { mRealOwner = realOwner; Class c = realOwner.getClass(); try { mMethod = c.getMethod(methodName, PARAM_TYPES); } catch (Exception e) { InflateException ex = new InflateException( "Couldn't resolve menu item onClick handler " + methodName + " in class " + c.getName()); ex.initCause(e); throw ex; } } public boolean onMenuItemClick(MenuItem item) { try { if (mMethod.getReturnType() == Boolean.TYPE) { return (Boolean) mMethod.invoke(mRealOwner, item); } else { mMethod.invoke(mRealOwner, item); return true; } } catch (Exception e) { throw new RuntimeException(e); } } } private Object getRealOwner() { if (mRealOwner == null) { mRealOwner = findRealOwner(mContext); } return mRealOwner; } private Object findRealOwner(Object owner) { if (owner instanceof Activity) { return owner; } if (owner instanceof ContextWrapper) { return findRealOwner(((ContextWrapper) owner).getBaseContext()); } return owner; } /** * State for the current menu. *

* Groups can not be nested unless there is another menu (which will have * its state class). */ private class MenuState { private Menu menu; /* * Group state is set on items as they are added, allowing an item to * override its group state. (As opposed to set on items at the group end tag.) */ private int groupId; private int groupCategory; private int groupOrder; private int groupCheckable; private boolean groupVisible; private boolean groupEnabled; private boolean itemAdded; private int itemId; private int itemCategoryOrder; private CharSequence itemTitle; private CharSequence itemTitleCondensed; private int itemIconResId; private char itemAlphabeticShortcut; private char itemNumericShortcut; /** * Sync to attrs.xml enum: * - 0: none * - 1: all * - 2: exclusive */ private int itemCheckable; private boolean itemChecked; private boolean itemVisible; private boolean itemEnabled; /** * Sync to attrs.xml enum, values in MenuItem: * - 0: never * - 1: ifRoom * - 2: always * - -1: Safe sentinel for "no value". */ private int itemShowAsAction; private int itemActionViewLayout; private String itemActionViewClassName; private String itemActionProviderClassName; private String itemListenerMethodName; private ActionProvider itemActionProvider; private static final int defaultGroupId = NO_ID; private static final int defaultItemId = NO_ID; private static final int defaultItemCategory = 0; private static final int defaultItemOrder = 0; private static final int defaultItemCheckable = 0; private static final boolean defaultItemChecked = false; private static final boolean defaultItemVisible = true; private static final boolean defaultItemEnabled = true; public MenuState(final Menu menu) { this.menu = menu; resetGroup(); } public void resetGroup() { groupId = defaultGroupId; groupCategory = defaultItemCategory; groupOrder = defaultItemOrder; groupCheckable = defaultItemCheckable; groupVisible = defaultItemVisible; groupEnabled = defaultItemEnabled; } /** * Called when the parser is pointing to a group tag. */ public void readGroup(AttributeSet attrs) { TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuGroup); groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId); groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory); groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder); groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable); groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible); groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled); a.recycle(); } /** * Called when the parser is pointing to an item tag. */ public void readItem(AttributeSet attrs) { TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuItem); // Inherit attributes from the group as default value itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId); final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory); final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder); itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK); itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title); itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed); itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0); itemAlphabeticShortcut = getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut)); itemNumericShortcut = getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut)); if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) { // Item has attribute checkable, use it itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0; } else { // Item does not have attribute, use the group's (group can have one more state // for checkable that represents the exclusive checkable) itemCheckable = groupCheckable; } itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1); itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0); itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass); itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass); final boolean hasActionProvider = itemActionProviderClassName != null; if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) { itemActionProvider = newInstance(itemActionProviderClassName, ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE, mActionProviderConstructorArguments); } else { if (hasActionProvider) { Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'." + " Action view already specified."); } itemActionProvider = null; } a.recycle(); itemAdded = false; } private char getShortcut(String shortcutString) { if (shortcutString == null) { return 0; } else { return shortcutString.charAt(0); } } private void setItem(MenuItem item) { item.setChecked(itemChecked) .setVisible(itemVisible) .setEnabled(itemEnabled) .setCheckable(itemCheckable >= 1) .setTitleCondensed(itemTitleCondensed) .setIcon(itemIconResId) .setAlphabeticShortcut(itemAlphabeticShortcut) .setNumericShortcut(itemNumericShortcut); if (itemShowAsAction >= 0) { item.setShowAsAction(itemShowAsAction); } if (itemListenerMethodName != null) { if (mContext.isRestricted()) { throw new IllegalStateException("The android:onClick attribute cannot " + "be used within a restricted context"); } item.setOnMenuItemClickListener( new InflatedOnMenuItemClickListener(getRealOwner(), itemListenerMethodName)); } if (item instanceof MenuItemImpl) { MenuItemImpl impl = (MenuItemImpl) item; if (itemCheckable >= 2) { impl.setExclusiveCheckable(true); } } boolean actionViewSpecified = false; if (itemActionViewClassName != null) { View actionView = (View) newInstance(itemActionViewClassName, ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments); item.setActionView(actionView); actionViewSpecified = true; } if (itemActionViewLayout > 0) { if (!actionViewSpecified) { item.setActionView(itemActionViewLayout); actionViewSpecified = true; } else { Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'." + " Action view already specified."); } } if (itemActionProvider != null) { item.setActionProvider(itemActionProvider); } } public MenuItem addItem() { itemAdded = true; MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle); setItem(item); return item; } public SubMenu addSubMenuItem() { itemAdded = true; SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); setItem(subMenu.getItem()); return subMenu; } public boolean hasAddedItem() { return itemAdded; } @SuppressWarnings("unchecked") private T newInstance(String className, Class[] constructorSignature, Object[] arguments) { try { Class clazz = mContext.getClassLoader().loadClass(className); Constructor constructor = clazz.getConstructor(constructorSignature); constructor.setAccessible(true); return (T) constructor.newInstance(arguments); } catch (Exception e) { Log.w(LOG_TAG, "Cannot instantiate class: " + className, e); } return null; } } }