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