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