1/*
2 * Copyright (C) 2006 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.view;
18
19import android.annotation.MenuRes;
20import android.app.Activity;
21import android.content.Context;
22import android.content.ContextWrapper;
23import android.content.res.ColorStateList;
24import android.content.res.TypedArray;
25import android.content.res.XmlResourceParser;
26import android.graphics.PorterDuff;
27import android.graphics.drawable.Drawable;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.util.Xml;
31
32import com.android.internal.view.menu.MenuItemImpl;
33
34import org.xmlpull.v1.XmlPullParser;
35import org.xmlpull.v1.XmlPullParserException;
36
37import java.io.IOException;
38import java.lang.reflect.Constructor;
39import java.lang.reflect.Method;
40
41/**
42 * This class is used to instantiate menu XML files into Menu objects.
43 * <p>
44 * For performance reasons, menu inflation relies heavily on pre-processing of
45 * XML files that is done at build time. Therefore, it is not currently possible
46 * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
47 * it only works with an XmlPullParser returned from a compiled resource (R.
48 * <em>something</em> file.)
49 */
50public class MenuInflater {
51    private static final String LOG_TAG = "MenuInflater";
52
53    /** Menu tag name in XML. */
54    private static final String XML_MENU = "menu";
55
56    /** Group tag name in XML. */
57    private static final String XML_GROUP = "group";
58
59    /** Item tag name in XML. */
60    private static final String XML_ITEM = "item";
61
62    private static final int NO_ID = 0;
63
64    private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class};
65
66    private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = ACTION_VIEW_CONSTRUCTOR_SIGNATURE;
67
68    private final Object[] mActionViewConstructorArguments;
69
70    private final Object[] mActionProviderConstructorArguments;
71
72    private Context mContext;
73    private Object mRealOwner;
74
75    /**
76     * Constructs a menu inflater.
77     *
78     * @see Activity#getMenuInflater()
79     */
80    public MenuInflater(Context context) {
81        mContext = context;
82        mActionViewConstructorArguments = new Object[] {context};
83        mActionProviderConstructorArguments = mActionViewConstructorArguments;
84    }
85
86    /**
87     * Constructs a menu inflater.
88     *
89     * @see Activity#getMenuInflater()
90     * @hide
91     */
92    public MenuInflater(Context context, Object realOwner) {
93        mContext = context;
94        mRealOwner = realOwner;
95        mActionViewConstructorArguments = new Object[] {context};
96        mActionProviderConstructorArguments = mActionViewConstructorArguments;
97    }
98
99    /**
100     * Inflate a menu hierarchy from the specified XML resource. Throws
101     * {@link InflateException} if there is an error.
102     *
103     * @param menuRes Resource ID for an XML layout resource to load (e.g.,
104     *            <code>R.menu.main_activity</code>)
105     * @param menu The Menu to inflate into. The items and submenus will be
106     *            added to this Menu.
107     */
108    public void inflate(@MenuRes int menuRes, Menu menu) {
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                        registerMenu(subMenu, attrs);
169
170                        // Parse the submenu into returned SubMenu
171                        parseMenu(parser, attrs, subMenu);
172                    } else {
173                        lookingForEndOfUnknownTag = true;
174                        unknownTagName = tagName;
175                    }
176                    break;
177
178                case XmlPullParser.END_TAG:
179                    tagName = parser.getName();
180                    if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
181                        lookingForEndOfUnknownTag = false;
182                        unknownTagName = null;
183                    } else if (tagName.equals(XML_GROUP)) {
184                        menuState.resetGroup();
185                    } else if (tagName.equals(XML_ITEM)) {
186                        // Add the item if it hasn't been added (if the item was
187                        // a submenu, it would have been added already)
188                        if (!menuState.hasAddedItem()) {
189                            if (menuState.itemActionProvider != null &&
190                                    menuState.itemActionProvider.hasSubMenu()) {
191                                registerMenu(menuState.addSubMenuItem(), attrs);
192                            } else {
193                                registerMenu(menuState.addItem(), attrs);
194                            }
195                        }
196                    } else if (tagName.equals(XML_MENU)) {
197                        reachedEndOfMenu = true;
198                    }
199                    break;
200
201                case XmlPullParser.END_DOCUMENT:
202                    throw new RuntimeException("Unexpected end of document");
203            }
204
205            eventType = parser.next();
206        }
207    }
208
209    /**
210     * The method is a hook for layoutlib to do its magic.
211     * Nothing is needed outside of LayoutLib. However, it should not be deleted because it
212     * appears to do nothing.
213     */
214    private void registerMenu(@SuppressWarnings("unused") MenuItem item,
215            @SuppressWarnings("unused") AttributeSet set) {
216    }
217
218    /**
219     * The method is a hook for layoutlib to do its magic.
220     * Nothing is needed outside of LayoutLib. However, it should not be deleted because it
221     * appears to do nothing.
222     */
223    private void registerMenu(@SuppressWarnings("unused") SubMenu subMenu,
224            @SuppressWarnings("unused") AttributeSet set) {
225    }
226
227    // Needed by layoutlib.
228    /*package*/ Context getContext() {
229        return mContext;
230    }
231
232    private static class InflatedOnMenuItemClickListener
233            implements MenuItem.OnMenuItemClickListener {
234        private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };
235
236        private Object mRealOwner;
237        private Method mMethod;
238
239        public InflatedOnMenuItemClickListener(Object realOwner, String methodName) {
240            mRealOwner = realOwner;
241            Class<?> c = realOwner.getClass();
242            try {
243                mMethod = c.getMethod(methodName, PARAM_TYPES);
244            } catch (Exception e) {
245                InflateException ex = new InflateException(
246                        "Couldn't resolve menu item onClick handler " + methodName +
247                        " in class " + c.getName());
248                ex.initCause(e);
249                throw ex;
250            }
251        }
252
253        public boolean onMenuItemClick(MenuItem item) {
254            try {
255                if (mMethod.getReturnType() == Boolean.TYPE) {
256                    return (Boolean) mMethod.invoke(mRealOwner, item);
257                } else {
258                    mMethod.invoke(mRealOwner, item);
259                    return true;
260                }
261            } catch (Exception e) {
262                throw new RuntimeException(e);
263            }
264        }
265    }
266
267    private Object getRealOwner() {
268        if (mRealOwner == null) {
269            mRealOwner = findRealOwner(mContext);
270        }
271        return mRealOwner;
272    }
273
274    private Object findRealOwner(Object owner) {
275        if (owner instanceof Activity) {
276            return owner;
277        }
278        if (owner instanceof ContextWrapper) {
279            return findRealOwner(((ContextWrapper) owner).getBaseContext());
280        }
281        return owner;
282    }
283
284    /**
285     * State for the current menu.
286     * <p>
287     * Groups can not be nested unless there is another menu (which will have
288     * its state class).
289     */
290    private class MenuState {
291        private Menu menu;
292
293        /*
294         * Group state is set on items as they are added, allowing an item to
295         * override its group state. (As opposed to set on items at the group end tag.)
296         */
297        private int groupId;
298        private int groupCategory;
299        private int groupOrder;
300        private int groupCheckable;
301        private boolean groupVisible;
302        private boolean groupEnabled;
303
304        private boolean itemAdded;
305        private int itemId;
306        private int itemCategoryOrder;
307        private CharSequence itemTitle;
308        private CharSequence itemTitleCondensed;
309        private int itemIconResId;
310        private ColorStateList itemIconTintList = null;
311        private PorterDuff.Mode itemIconTintMode = null;
312        private char itemAlphabeticShortcut;
313        private int itemAlphabeticModifiers;
314        private char itemNumericShortcut;
315        private int itemNumericModifiers;
316        /**
317         * Sync to attrs.xml enum:
318         * - 0: none
319         * - 1: all
320         * - 2: exclusive
321         */
322        private int itemCheckable;
323        private boolean itemChecked;
324        private boolean itemVisible;
325        private boolean itemEnabled;
326
327        /**
328         * Sync to attrs.xml enum, values in MenuItem:
329         * - 0: never
330         * - 1: ifRoom
331         * - 2: always
332         * - -1: Safe sentinel for "no value".
333         */
334        private int itemShowAsAction;
335
336        private int itemActionViewLayout;
337        private String itemActionViewClassName;
338        private String itemActionProviderClassName;
339
340        private String itemListenerMethodName;
341
342        private ActionProvider itemActionProvider;
343
344        private CharSequence itemContentDescription;
345        private CharSequence itemTooltipText;
346
347        private static final int defaultGroupId = NO_ID;
348        private static final int defaultItemId = NO_ID;
349        private static final int defaultItemCategory = 0;
350        private static final int defaultItemOrder = 0;
351        private static final int defaultItemCheckable = 0;
352        private static final boolean defaultItemChecked = false;
353        private static final boolean defaultItemVisible = true;
354        private static final boolean defaultItemEnabled = true;
355
356        public MenuState(final Menu menu) {
357            this.menu = menu;
358
359            resetGroup();
360        }
361
362        public void resetGroup() {
363            groupId = defaultGroupId;
364            groupCategory = defaultItemCategory;
365            groupOrder = defaultItemOrder;
366            groupCheckable = defaultItemCheckable;
367            groupVisible = defaultItemVisible;
368            groupEnabled = defaultItemEnabled;
369        }
370
371        /**
372         * Called when the parser is pointing to a group tag.
373         */
374        public void readGroup(AttributeSet attrs) {
375            TypedArray a = mContext.obtainStyledAttributes(attrs,
376                    com.android.internal.R.styleable.MenuGroup);
377
378            groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId);
379            groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory);
380            groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder);
381            groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable);
382            groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible);
383            groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled);
384
385            a.recycle();
386        }
387
388        /**
389         * Called when the parser is pointing to an item tag.
390         */
391        public void readItem(AttributeSet attrs) {
392            TypedArray a = mContext.obtainStyledAttributes(attrs,
393                    com.android.internal.R.styleable.MenuItem);
394
395            // Inherit attributes from the group as default value
396            itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);
397            final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);
398            final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder);
399            itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
400            itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title);
401            itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed);
402            itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0);
403            if (a.hasValue(com.android.internal.R.styleable.MenuItem_iconTintMode)) {
404                itemIconTintMode = Drawable.parseTintMode(a.getInt(
405                        com.android.internal.R.styleable.MenuItem_iconTintMode, -1),
406                        itemIconTintMode);
407            } else {
408                // Reset to null so that it's not carried over to the next item
409                itemIconTintMode = null;
410            }
411            if (a.hasValue(com.android.internal.R.styleable.MenuItem_iconTint)) {
412                itemIconTintList = a.getColorStateList(
413                        com.android.internal.R.styleable.MenuItem_iconTint);
414            } else {
415                // Reset to null so that it's not carried over to the next item
416                itemIconTintList = null;
417            }
418
419            itemAlphabeticShortcut =
420                    getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut));
421            itemAlphabeticModifiers =
422                    a.getInt(com.android.internal.R.styleable.MenuItem_alphabeticModifiers,
423                            KeyEvent.META_CTRL_ON);
424            itemNumericShortcut =
425                    getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut));
426            itemNumericModifiers =
427                    a.getInt(com.android.internal.R.styleable.MenuItem_numericModifiers,
428                            KeyEvent.META_CTRL_ON);
429            if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) {
430                // Item has attribute checkable, use it
431                itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0;
432            } else {
433                // Item does not have attribute, use the group's (group can have one more state
434                // for checkable that represents the exclusive checkable)
435                itemCheckable = groupCheckable;
436            }
437            itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked);
438            itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible);
439            itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled);
440            itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1);
441            itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick);
442            itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0);
443            itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass);
444            itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass);
445
446            final boolean hasActionProvider = itemActionProviderClassName != null;
447            if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
448                itemActionProvider = newInstance(itemActionProviderClassName,
449                            ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
450                            mActionProviderConstructorArguments);
451            } else {
452                if (hasActionProvider) {
453                    Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
454                            + " Action view already specified.");
455                }
456                itemActionProvider = null;
457            }
458
459            itemContentDescription =
460                    a.getText(com.android.internal.R.styleable.MenuItem_contentDescription);
461            itemTooltipText = a.getText(com.android.internal.R.styleable.MenuItem_tooltipText);
462
463            a.recycle();
464
465            itemAdded = false;
466        }
467
468        private char getShortcut(String shortcutString) {
469            if (shortcutString == null) {
470                return 0;
471            } else {
472                return shortcutString.charAt(0);
473            }
474        }
475
476        private void setItem(MenuItem item) {
477            item.setChecked(itemChecked)
478                .setVisible(itemVisible)
479                .setEnabled(itemEnabled)
480                .setCheckable(itemCheckable >= 1)
481                .setTitleCondensed(itemTitleCondensed)
482                .setIcon(itemIconResId)
483                .setAlphabeticShortcut(itemAlphabeticShortcut, itemAlphabeticModifiers)
484                .setNumericShortcut(itemNumericShortcut, itemNumericModifiers);
485
486            if (itemShowAsAction >= 0) {
487                item.setShowAsAction(itemShowAsAction);
488            }
489
490            if (itemIconTintMode != null) {
491                item.setIconTintMode(itemIconTintMode);
492            }
493
494            if (itemIconTintList != null) {
495                item.setIconTintList(itemIconTintList);
496            }
497
498            if (itemListenerMethodName != null) {
499                if (mContext.isRestricted()) {
500                    throw new IllegalStateException("The android:onClick attribute cannot "
501                            + "be used within a restricted context");
502                }
503                item.setOnMenuItemClickListener(
504                        new InflatedOnMenuItemClickListener(getRealOwner(), itemListenerMethodName));
505            }
506
507            if (item instanceof MenuItemImpl) {
508                MenuItemImpl impl = (MenuItemImpl) item;
509                if (itemCheckable >= 2) {
510                    impl.setExclusiveCheckable(true);
511                }
512            }
513
514            boolean actionViewSpecified = false;
515            if (itemActionViewClassName != null) {
516                View actionView = (View) newInstance(itemActionViewClassName,
517                        ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
518                item.setActionView(actionView);
519                actionViewSpecified = true;
520            }
521            if (itemActionViewLayout > 0) {
522                if (!actionViewSpecified) {
523                    item.setActionView(itemActionViewLayout);
524                    actionViewSpecified = true;
525                } else {
526                    Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."
527                            + " Action view already specified.");
528                }
529            }
530            if (itemActionProvider != null) {
531                item.setActionProvider(itemActionProvider);
532            }
533
534            item.setContentDescription(itemContentDescription);
535            item.setTooltipText(itemTooltipText);
536        }
537
538        public MenuItem addItem() {
539            itemAdded = true;
540            MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle);
541            setItem(item);
542            return item;
543        }
544
545        public SubMenu addSubMenuItem() {
546            itemAdded = true;
547            SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
548            setItem(subMenu.getItem());
549            return subMenu;
550        }
551
552        public boolean hasAddedItem() {
553            return itemAdded;
554        }
555
556        @SuppressWarnings("unchecked")
557        private <T> T newInstance(String className, Class<?>[] constructorSignature,
558                Object[] arguments) {
559            try {
560                Class<?> clazz = mContext.getClassLoader().loadClass(className);
561                Constructor<?> constructor = clazz.getConstructor(constructorSignature);
562                constructor.setAccessible(true);
563                return (T) constructor.newInstance(arguments);
564            } catch (Exception e) {
565                Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
566            }
567            return null;
568        }
569    }
570}
571