Home | History | Annotate | Download | only in view
      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 android.view;
     18 
     19 import com.android.internal.view.menu.MenuItemImpl;
     20 
     21 import org.xmlpull.v1.XmlPullParser;
     22 import org.xmlpull.v1.XmlPullParserException;
     23 
     24 import android.app.Activity;
     25 import android.content.Context;
     26 import android.content.res.TypedArray;
     27 import android.content.res.XmlResourceParser;
     28 import android.util.AttributeSet;
     29 import android.util.Log;
     30 import android.util.Xml;
     31 
     32 import java.io.IOException;
     33 import java.lang.reflect.Constructor;
     34 import java.lang.reflect.Method;
     35 
     36 /**
     37  * This class is used to instantiate menu XML files into Menu objects.
     38  * <p>
     39  * For performance reasons, menu inflation relies heavily on pre-processing of
     40  * XML files that is done at build time. Therefore, it is not currently possible
     41  * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
     42  * it only works with an XmlPullParser returned from a compiled resource (R.
     43  * <em>something</em> file.)
     44  */
     45 public class MenuInflater {
     46     private static final String LOG_TAG = "MenuInflater";
     47 
     48     /** Menu tag name in XML. */
     49     private static final String XML_MENU = "menu";
     50 
     51     /** Group tag name in XML. */
     52     private static final String XML_GROUP = "group";
     53 
     54     /** Item tag name in XML. */
     55     private static final String XML_ITEM = "item";
     56 
     57     private static final int NO_ID = 0;
     58 
     59     private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class};
     60 
     61     private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = ACTION_VIEW_CONSTRUCTOR_SIGNATURE;
     62 
     63     private final Object[] mActionViewConstructorArguments;
     64 
     65     private final Object[] mActionProviderConstructorArguments;
     66 
     67     private Context mContext;
     68 
     69     /**
     70      * Constructs a menu inflater.
     71      *
     72      * @see Activity#getMenuInflater()
     73      */
     74     public MenuInflater(Context context) {
     75         mContext = context;
     76         mActionViewConstructorArguments = new Object[] {context};
     77         mActionProviderConstructorArguments = mActionViewConstructorArguments;
     78     }
     79 
     80     /**
     81      * Inflate a menu hierarchy from the specified XML resource. Throws
     82      * {@link InflateException} if there is an error.
     83      *
     84      * @param menuRes Resource ID for an XML layout resource to load (e.g.,
     85      *            <code>R.menu.main_activity</code>)
     86      * @param menu The Menu to inflate into. The items and submenus will be
     87      *            added to this Menu.
     88      */
     89     public void inflate(int menuRes, Menu menu) {
     90         XmlResourceParser parser = null;
     91         try {
     92             parser = mContext.getResources().getLayout(menuRes);
     93             AttributeSet attrs = Xml.asAttributeSet(parser);
     94 
     95             parseMenu(parser, attrs, menu);
     96         } catch (XmlPullParserException e) {
     97             throw new InflateException("Error inflating menu XML", e);
     98         } catch (IOException e) {
     99             throw new InflateException("Error inflating menu XML", e);
    100         } finally {
    101             if (parser != null) parser.close();
    102         }
    103     }
    104 
    105     /**
    106      * Called internally to fill the given menu. If a sub menu is seen, it will
    107      * call this recursively.
    108      */
    109     private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
    110             throws XmlPullParserException, IOException {
    111         MenuState menuState = new MenuState(menu);
    112 
    113         int eventType = parser.getEventType();
    114         String tagName;
    115         boolean lookingForEndOfUnknownTag = false;
    116         String unknownTagName = null;
    117 
    118         // This loop will skip to the menu start tag
    119         do {
    120             if (eventType == XmlPullParser.START_TAG) {
    121                 tagName = parser.getName();
    122                 if (tagName.equals(XML_MENU)) {
    123                     // Go to next tag
    124                     eventType = parser.next();
    125                     break;
    126                 }
    127 
    128                 throw new RuntimeException("Expecting menu, got " + tagName);
    129             }
    130             eventType = parser.next();
    131         } while (eventType != XmlPullParser.END_DOCUMENT);
    132 
    133         boolean reachedEndOfMenu = false;
    134         while (!reachedEndOfMenu) {
    135             switch (eventType) {
    136                 case XmlPullParser.START_TAG:
    137                     if (lookingForEndOfUnknownTag) {
    138                         break;
    139                     }
    140 
    141                     tagName = parser.getName();
    142                     if (tagName.equals(XML_GROUP)) {
    143                         menuState.readGroup(attrs);
    144                     } else if (tagName.equals(XML_ITEM)) {
    145                         menuState.readItem(attrs);
    146                     } else if (tagName.equals(XML_MENU)) {
    147                         // A menu start tag denotes a submenu for an item
    148                         SubMenu subMenu = menuState.addSubMenuItem();
    149 
    150                         // Parse the submenu into returned SubMenu
    151                         parseMenu(parser, attrs, subMenu);
    152                     } else {
    153                         lookingForEndOfUnknownTag = true;
    154                         unknownTagName = tagName;
    155                     }
    156                     break;
    157 
    158                 case XmlPullParser.END_TAG:
    159                     tagName = parser.getName();
    160                     if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
    161                         lookingForEndOfUnknownTag = false;
    162                         unknownTagName = null;
    163                     } else if (tagName.equals(XML_GROUP)) {
    164                         menuState.resetGroup();
    165                     } else if (tagName.equals(XML_ITEM)) {
    166                         // Add the item if it hasn't been added (if the item was
    167                         // a submenu, it would have been added already)
    168                         if (!menuState.hasAddedItem()) {
    169                             if (menuState.itemActionProvider != null &&
    170                                     menuState.itemActionProvider.hasSubMenu()) {
    171                                 menuState.addSubMenuItem();
    172                             } else {
    173                                 menuState.addItem();
    174                             }
    175                         }
    176                     } else if (tagName.equals(XML_MENU)) {
    177                         reachedEndOfMenu = true;
    178                     }
    179                     break;
    180 
    181                 case XmlPullParser.END_DOCUMENT:
    182                     throw new RuntimeException("Unexpected end of document");
    183             }
    184 
    185             eventType = parser.next();
    186         }
    187     }
    188 
    189     private static class InflatedOnMenuItemClickListener
    190             implements MenuItem.OnMenuItemClickListener {
    191         private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };
    192 
    193         private Context mContext;
    194         private Method mMethod;
    195 
    196         public InflatedOnMenuItemClickListener(Context context, String methodName) {
    197             mContext = context;
    198             Class<?> c = context.getClass();
    199             try {
    200                 mMethod = c.getMethod(methodName, PARAM_TYPES);
    201             } catch (Exception e) {
    202                 InflateException ex = new InflateException(
    203                         "Couldn't resolve menu item onClick handler " + methodName +
    204                         " in class " + c.getName());
    205                 ex.initCause(e);
    206                 throw ex;
    207             }
    208         }
    209 
    210         public boolean onMenuItemClick(MenuItem item) {
    211             try {
    212                 if (mMethod.getReturnType() == Boolean.TYPE) {
    213                     return (Boolean) mMethod.invoke(mContext, item);
    214                 } else {
    215                     mMethod.invoke(mContext, item);
    216                     return true;
    217                 }
    218             } catch (Exception e) {
    219                 throw new RuntimeException(e);
    220             }
    221         }
    222     }
    223 
    224     /**
    225      * State for the current menu.
    226      * <p>
    227      * Groups can not be nested unless there is another menu (which will have
    228      * its state class).
    229      */
    230     private class MenuState {
    231         private Menu menu;
    232 
    233         /*
    234          * Group state is set on items as they are added, allowing an item to
    235          * override its group state. (As opposed to set on items at the group end tag.)
    236          */
    237         private int groupId;
    238         private int groupCategory;
    239         private int groupOrder;
    240         private int groupCheckable;
    241         private boolean groupVisible;
    242         private boolean groupEnabled;
    243 
    244         private boolean itemAdded;
    245         private int itemId;
    246         private int itemCategoryOrder;
    247         private CharSequence itemTitle;
    248         private CharSequence itemTitleCondensed;
    249         private int itemIconResId;
    250         private char itemAlphabeticShortcut;
    251         private char itemNumericShortcut;
    252         /**
    253          * Sync to attrs.xml enum:
    254          * - 0: none
    255          * - 1: all
    256          * - 2: exclusive
    257          */
    258         private int itemCheckable;
    259         private boolean itemChecked;
    260         private boolean itemVisible;
    261         private boolean itemEnabled;
    262 
    263         /**
    264          * Sync to attrs.xml enum, values in MenuItem:
    265          * - 0: never
    266          * - 1: ifRoom
    267          * - 2: always
    268          * - -1: Safe sentinel for "no value".
    269          */
    270         private int itemShowAsAction;
    271 
    272         private int itemActionViewLayout;
    273         private String itemActionViewClassName;
    274         private String itemActionProviderClassName;
    275 
    276         private String itemListenerMethodName;
    277 
    278         private ActionProvider itemActionProvider;
    279 
    280         private static final int defaultGroupId = NO_ID;
    281         private static final int defaultItemId = NO_ID;
    282         private static final int defaultItemCategory = 0;
    283         private static final int defaultItemOrder = 0;
    284         private static final int defaultItemCheckable = 0;
    285         private static final boolean defaultItemChecked = false;
    286         private static final boolean defaultItemVisible = true;
    287         private static final boolean defaultItemEnabled = true;
    288 
    289         public MenuState(final Menu menu) {
    290             this.menu = menu;
    291 
    292             resetGroup();
    293         }
    294 
    295         public void resetGroup() {
    296             groupId = defaultGroupId;
    297             groupCategory = defaultItemCategory;
    298             groupOrder = defaultItemOrder;
    299             groupCheckable = defaultItemCheckable;
    300             groupVisible = defaultItemVisible;
    301             groupEnabled = defaultItemEnabled;
    302         }
    303 
    304         /**
    305          * Called when the parser is pointing to a group tag.
    306          */
    307         public void readGroup(AttributeSet attrs) {
    308             TypedArray a = mContext.obtainStyledAttributes(attrs,
    309                     com.android.internal.R.styleable.MenuGroup);
    310 
    311             groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId);
    312             groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory);
    313             groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder);
    314             groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable);
    315             groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible);
    316             groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled);
    317 
    318             a.recycle();
    319         }
    320 
    321         /**
    322          * Called when the parser is pointing to an item tag.
    323          */
    324         public void readItem(AttributeSet attrs) {
    325             TypedArray a = mContext.obtainStyledAttributes(attrs,
    326                     com.android.internal.R.styleable.MenuItem);
    327 
    328             // Inherit attributes from the group as default value
    329             itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);
    330             final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);
    331             final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder);
    332             itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
    333             itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title);
    334             itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed);
    335             itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0);
    336             itemAlphabeticShortcut =
    337                     getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut));
    338             itemNumericShortcut =
    339                     getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut));
    340             if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) {
    341                 // Item has attribute checkable, use it
    342                 itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0;
    343             } else {
    344                 // Item does not have attribute, use the group's (group can have one more state
    345                 // for checkable that represents the exclusive checkable)
    346                 itemCheckable = groupCheckable;
    347             }
    348             itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked);
    349             itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible);
    350             itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled);
    351             itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1);
    352             itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick);
    353             itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0);
    354             itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass);
    355             itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass);
    356 
    357             final boolean hasActionProvider = itemActionProviderClassName != null;
    358             if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
    359                 itemActionProvider = newInstance(itemActionProviderClassName,
    360                             ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
    361                             mActionProviderConstructorArguments);
    362             } else {
    363                 if (hasActionProvider) {
    364                     Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
    365                             + " Action view already specified.");
    366                 }
    367                 itemActionProvider = null;
    368             }
    369 
    370             a.recycle();
    371 
    372             itemAdded = false;
    373         }
    374 
    375         private char getShortcut(String shortcutString) {
    376             if (shortcutString == null) {
    377                 return 0;
    378             } else {
    379                 return shortcutString.charAt(0);
    380             }
    381         }
    382 
    383         private void setItem(MenuItem item) {
    384             item.setChecked(itemChecked)
    385                 .setVisible(itemVisible)
    386                 .setEnabled(itemEnabled)
    387                 .setCheckable(itemCheckable >= 1)
    388                 .setTitleCondensed(itemTitleCondensed)
    389                 .setIcon(itemIconResId)
    390                 .setAlphabeticShortcut(itemAlphabeticShortcut)
    391                 .setNumericShortcut(itemNumericShortcut);
    392 
    393             if (itemShowAsAction >= 0) {
    394                 item.setShowAsAction(itemShowAsAction);
    395             }
    396 
    397             if (itemListenerMethodName != null) {
    398                 if (mContext.isRestricted()) {
    399                     throw new IllegalStateException("The android:onClick attribute cannot "
    400                             + "be used within a restricted context");
    401                 }
    402                 item.setOnMenuItemClickListener(
    403                         new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName));
    404             }
    405 
    406             if (item instanceof MenuItemImpl) {
    407                 MenuItemImpl impl = (MenuItemImpl) item;
    408                 if (itemCheckable >= 2) {
    409                     impl.setExclusiveCheckable(true);
    410                 }
    411             }
    412 
    413             boolean actionViewSpecified = false;
    414             if (itemActionViewClassName != null) {
    415                 View actionView = (View) newInstance(itemActionViewClassName,
    416                         ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
    417                 item.setActionView(actionView);
    418                 actionViewSpecified = true;
    419             }
    420             if (itemActionViewLayout > 0) {
    421                 if (!actionViewSpecified) {
    422                     item.setActionView(itemActionViewLayout);
    423                     actionViewSpecified = true;
    424                 } else {
    425                     Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."
    426                             + " Action view already specified.");
    427                 }
    428             }
    429             if (itemActionProvider != null) {
    430                 item.setActionProvider(itemActionProvider);
    431             }
    432         }
    433 
    434         public void addItem() {
    435             itemAdded = true;
    436             setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
    437         }
    438 
    439         public SubMenu addSubMenuItem() {
    440             itemAdded = true;
    441             SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
    442             setItem(subMenu.getItem());
    443             return subMenu;
    444         }
    445 
    446         public boolean hasAddedItem() {
    447             return itemAdded;
    448         }
    449 
    450         @SuppressWarnings("unchecked")
    451         private <T> T newInstance(String className, Class<?>[] constructorSignature,
    452                 Object[] arguments) {
    453             try {
    454                 Class<?> clazz = mContext.getClassLoader().loadClass(className);
    455                 Constructor<?> constructor = clazz.getConstructor(constructorSignature);
    456                 return (T) constructor.newInstance(arguments);
    457             } catch (Exception e) {
    458                 Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
    459             }
    460             return null;
    461         }
    462     }
    463 }
    464