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