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
17package androidx.appcompat.view;
18
19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.app.Activity;
22import android.content.Context;
23import android.content.ContextWrapper;
24import android.content.res.ColorStateList;
25import android.content.res.TypedArray;
26import android.content.res.XmlResourceParser;
27import android.graphics.PorterDuff;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.util.Xml;
31import android.view.InflateException;
32import android.view.KeyEvent;
33import android.view.Menu;
34import android.view.MenuInflater;
35import android.view.MenuItem;
36import android.view.SubMenu;
37import android.view.View;
38
39import androidx.annotation.LayoutRes;
40import androidx.annotation.RestrictTo;
41import androidx.appcompat.R;
42import androidx.appcompat.view.menu.MenuItemImpl;
43import androidx.appcompat.view.menu.MenuItemWrapperICS;
44import androidx.appcompat.widget.DrawableUtils;
45import androidx.core.internal.view.SupportMenu;
46import androidx.core.view.ActionProvider;
47import androidx.core.view.MenuItemCompat;
48
49import org.xmlpull.v1.XmlPullParser;
50import org.xmlpull.v1.XmlPullParserException;
51
52import java.io.IOException;
53import java.lang.reflect.Constructor;
54import 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)
68public 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