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