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