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