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