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