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.v4.internal.view.SupportMenu;
28import android.support.v4.view.ActionProvider;
29import android.support.v4.view.MenuItemCompat;
30import android.support.v7.appcompat.R;
31import android.support.v7.view.menu.MenuItemImpl;
32import android.support.v7.view.menu.MenuItemWrapperICS;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.util.Xml;
36import android.view.InflateException;
37import android.view.Menu;
38import android.view.MenuInflater;
39import android.view.MenuItem;
40import android.view.SubMenu;
41import android.view.View;
42
43import java.io.IOException;
44import java.lang.reflect.Constructor;
45import java.lang.reflect.Method;
46
47/**
48 * This class is used to instantiate menu XML files into Menu objects.
49 * <p>
50 * For performance reasons, menu inflation relies heavily on pre-processing of
51 * XML files that is done at build time. Therefore, it is not currently possible
52 * to use SupportMenuInflater with an XmlPullParser over a plain XML file at runtime;
53 * it only works with an XmlPullParser returned from a compiled resource (R.
54 * <em>something</em> file.)
55 *
56 * @hide
57 */
58public class SupportMenuInflater extends MenuInflater {
59    private static final String LOG_TAG = "SupportMenuInflater";
60
61    /** Menu tag name in XML. */
62    private static final String XML_MENU = "menu";
63
64    /** Group tag name in XML. */
65    private static final String XML_GROUP = "group";
66
67    /** Item tag name in XML. */
68    private static final String XML_ITEM = "item";
69
70    private static final int NO_ID = 0;
71
72    private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class};
73
74    private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE =
75            ACTION_VIEW_CONSTRUCTOR_SIGNATURE;
76
77    private final Object[] mActionViewConstructorArguments;
78
79    private final Object[] mActionProviderConstructorArguments;
80
81    private Context mContext;
82    private Object mRealOwner;
83
84    /**
85     * Constructs a menu inflater.
86     *
87     * @see Activity#getMenuInflater()
88     */
89    public SupportMenuInflater(Context context) {
90        super(context);
91        mContext = 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 Object getRealOwner() {
213        if (mRealOwner == null) {
214            mRealOwner = findRealOwner(mContext);
215        }
216        return mRealOwner;
217    }
218
219    private Object findRealOwner(Object owner) {
220        if (owner instanceof Activity) {
221            return owner;
222        }
223        if (owner instanceof ContextWrapper) {
224            return findRealOwner(((ContextWrapper) owner).getBaseContext());
225        }
226        return owner;
227    }
228
229    private static class InflatedOnMenuItemClickListener
230            implements MenuItem.OnMenuItemClickListener {
231        private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };
232
233        private Object mRealOwner;
234        private Method mMethod;
235
236        public InflatedOnMenuItemClickListener(Object realOwner, String methodName) {
237            mRealOwner = realOwner;
238            Class<?> c = realOwner.getClass();
239            try {
240                mMethod = c.getMethod(methodName, PARAM_TYPES);
241            } catch (Exception e) {
242                InflateException ex = new InflateException(
243                        "Couldn't resolve menu item onClick handler " + methodName +
244                                " in class " + c.getName());
245                ex.initCause(e);
246                throw ex;
247            }
248        }
249
250        public boolean onMenuItemClick(MenuItem item) {
251            try {
252                if (mMethod.getReturnType() == Boolean.TYPE) {
253                    return (Boolean) mMethod.invoke(mRealOwner, item);
254                } else {
255                    mMethod.invoke(mRealOwner, item);
256                    return true;
257                }
258            } catch (Exception e) {
259                throw new RuntimeException(e);
260            }
261        }
262    }
263
264    /**
265     * State for the current menu.
266     * <p>
267     * Groups can not be nested unless there is another menu (which will have
268     * its state class).
269     */
270    private class MenuState {
271        private Menu menu;
272
273        /*
274         * Group state is set on items as they are added, allowing an item to
275         * override its group state. (As opposed to set on items at the group end tag.)
276         */
277        private int groupId;
278        private int groupCategory;
279        private int groupOrder;
280        private int groupCheckable;
281        private boolean groupVisible;
282        private boolean groupEnabled;
283
284        private boolean itemAdded;
285        private int itemId;
286        private int itemCategoryOrder;
287        private CharSequence itemTitle;
288        private CharSequence itemTitleCondensed;
289        private int itemIconResId;
290        private char itemAlphabeticShortcut;
291        private char itemNumericShortcut;
292        /**
293         * Sync to attrs.xml enum:
294         * - 0: none
295         * - 1: all
296         * - 2: exclusive
297         */
298        private int itemCheckable;
299        private boolean itemChecked;
300        private boolean itemVisible;
301        private boolean itemEnabled;
302
303        /**
304         * Sync to attrs.xml enum, values in MenuItem:
305         * - 0: never
306         * - 1: ifRoom
307         * - 2: always
308         * - -1: Safe sentinel for "no value".
309         */
310        private int itemShowAsAction;
311
312        private int itemActionViewLayout;
313        private String itemActionViewClassName;
314        private String itemActionProviderClassName;
315
316        private String itemListenerMethodName;
317
318        private ActionProvider itemActionProvider;
319
320        private static final int defaultGroupId = NO_ID;
321        private static final int defaultItemId = NO_ID;
322        private static final int defaultItemCategory = 0;
323        private static final int defaultItemOrder = 0;
324        private static final int defaultItemCheckable = 0;
325        private static final boolean defaultItemChecked = false;
326        private static final boolean defaultItemVisible = true;
327        private static final boolean defaultItemEnabled = true;
328
329        public MenuState(final Menu menu) {
330            this.menu = menu;
331
332            resetGroup();
333        }
334
335        public void resetGroup() {
336            groupId = defaultGroupId;
337            groupCategory = defaultItemCategory;
338            groupOrder = defaultItemOrder;
339            groupCheckable = defaultItemCheckable;
340            groupVisible = defaultItemVisible;
341            groupEnabled = defaultItemEnabled;
342        }
343
344        /**
345         * Called when the parser is pointing to a group tag.
346         */
347        public void readGroup(AttributeSet attrs) {
348            TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuGroup);
349
350            groupId = a.getResourceId(R.styleable.MenuGroup_android_id, defaultGroupId);
351            groupCategory = a.getInt(
352                    R.styleable.MenuGroup_android_menuCategory, defaultItemCategory);
353            groupOrder = a.getInt(R.styleable.MenuGroup_android_orderInCategory, defaultItemOrder);
354            groupCheckable = a.getInt(
355                    R.styleable.MenuGroup_android_checkableBehavior, defaultItemCheckable);
356            groupVisible = a.getBoolean(R.styleable.MenuGroup_android_visible, defaultItemVisible);
357            groupEnabled = a.getBoolean(R.styleable.MenuGroup_android_enabled, defaultItemEnabled);
358
359            a.recycle();
360        }
361
362        /**
363         * Called when the parser is pointing to an item tag.
364         */
365        public void readItem(AttributeSet attrs) {
366            TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuItem);
367
368            // Inherit attributes from the group as default value
369            itemId = a.getResourceId(R.styleable.MenuItem_android_id, defaultItemId);
370            final int category = a.getInt(R.styleable.MenuItem_android_menuCategory, groupCategory);
371            final int order = a.getInt(R.styleable.MenuItem_android_orderInCategory, groupOrder);
372            itemCategoryOrder = (category & SupportMenu.CATEGORY_MASK) |
373                    (order & SupportMenu.USER_MASK);
374            itemTitle = a.getText(R.styleable.MenuItem_android_title);
375            itemTitleCondensed = a.getText(R.styleable.MenuItem_android_titleCondensed);
376            itemIconResId = a.getResourceId(R.styleable.MenuItem_android_icon, 0);
377            itemAlphabeticShortcut =
378                    getShortcut(a.getString(R.styleable.MenuItem_android_alphabeticShortcut));
379            itemNumericShortcut =
380                    getShortcut(a.getString(R.styleable.MenuItem_android_numericShortcut));
381            if (a.hasValue(R.styleable.MenuItem_android_checkable)) {
382                // Item has attribute checkable, use it
383                itemCheckable = a.getBoolean(R.styleable.MenuItem_android_checkable, false) ? 1 : 0;
384            } else {
385                // Item does not have attribute, use the group's (group can have one more state
386                // for checkable that represents the exclusive checkable)
387                itemCheckable = groupCheckable;
388            }
389            itemChecked = a.getBoolean(R.styleable.MenuItem_android_checked, defaultItemChecked);
390            itemVisible = a.getBoolean(R.styleable.MenuItem_android_visible, groupVisible);
391            itemEnabled = a.getBoolean(R.styleable.MenuItem_android_enabled, groupEnabled);
392            itemShowAsAction = a.getInt(R.styleable.MenuItem_showAsAction, -1);
393            itemListenerMethodName = a.getString(R.styleable.MenuItem_android_onClick);
394            itemActionViewLayout = a.getResourceId(R.styleable.MenuItem_actionLayout, 0);
395            itemActionViewClassName = a.getString(R.styleable.MenuItem_actionViewClass);
396            itemActionProviderClassName = a.getString(R.styleable.MenuItem_actionProviderClass);
397
398            final boolean hasActionProvider = itemActionProviderClassName != null;
399            if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
400                itemActionProvider = newInstance(itemActionProviderClassName,
401                        ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
402                        mActionProviderConstructorArguments);
403            } else {
404                if (hasActionProvider) {
405                    Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
406                            + " Action view already specified.");
407                }
408                itemActionProvider = null;
409            }
410
411            a.recycle();
412
413            itemAdded = false;
414        }
415
416        private char getShortcut(String shortcutString) {
417            if (shortcutString == null) {
418                return 0;
419            } else {
420                return shortcutString.charAt(0);
421            }
422        }
423
424        private void setItem(MenuItem item) {
425            item.setChecked(itemChecked)
426                    .setVisible(itemVisible)
427                    .setEnabled(itemEnabled)
428                    .setCheckable(itemCheckable >= 1)
429                    .setTitleCondensed(itemTitleCondensed)
430                    .setIcon(itemIconResId)
431                    .setAlphabeticShortcut(itemAlphabeticShortcut)
432                    .setNumericShortcut(itemNumericShortcut);
433
434            if (itemShowAsAction >= 0) {
435                MenuItemCompat.setShowAsAction(item, itemShowAsAction);
436            }
437
438            if (itemListenerMethodName != null) {
439                if (mContext.isRestricted()) {
440                    throw new IllegalStateException("The android:onClick attribute cannot "
441                            + "be used within a restricted context");
442                }
443                item.setOnMenuItemClickListener(
444                        new InflatedOnMenuItemClickListener(getRealOwner(), itemListenerMethodName));
445            }
446
447            final MenuItemImpl impl = item instanceof MenuItemImpl ? (MenuItemImpl) item : null;
448            if (itemCheckable >= 2) {
449                if (item instanceof MenuItemImpl) {
450                    ((MenuItemImpl) item).setExclusiveCheckable(true);
451                } else if (item instanceof MenuItemWrapperICS) {
452                    ((MenuItemWrapperICS) item).setExclusiveCheckable(true);
453                }
454            }
455
456            boolean actionViewSpecified = false;
457            if (itemActionViewClassName != null) {
458                View actionView = (View) newInstance(itemActionViewClassName,
459                        ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
460                MenuItemCompat.setActionView(item, actionView);
461                actionViewSpecified = true;
462            }
463            if (itemActionViewLayout > 0) {
464                if (!actionViewSpecified) {
465                    MenuItemCompat.setActionView(item, itemActionViewLayout);
466                    actionViewSpecified = true;
467                } else {
468                    Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."
469                            + " Action view already specified.");
470                }
471            }
472            if (itemActionProvider != null) {
473                MenuItemCompat.setActionProvider(item, itemActionProvider);
474            }
475        }
476
477        public void addItem() {
478            itemAdded = true;
479            setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
480        }
481
482        public SubMenu addSubMenuItem() {
483            itemAdded = true;
484            SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
485            setItem(subMenu.getItem());
486            return subMenu;
487        }
488
489        public boolean hasAddedItem() {
490            return itemAdded;
491        }
492
493        @SuppressWarnings("unchecked")
494        private <T> T newInstance(String className, Class<?>[] constructorSignature,
495                Object[] arguments) {
496            try {
497                Class<?> clazz = mContext.getClassLoader().loadClass(className);
498                Constructor<?> constructor = clazz.getConstructor(constructorSignature);
499                constructor.setAccessible(true);
500                return (T) constructor.newInstance(arguments);
501            } catch (Exception e) {
502                Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
503            }
504            return null;
505        }
506    }
507}
508