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