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