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