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