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    private Object mRealOwner;
69
70    /**
71     * Constructs a menu inflater.
72     *
73     * @see Activity#getMenuInflater()
74     */
75    public MenuInflater(Context context) {
76        mContext = context;
77        mRealOwner = 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
165                        // Parse the submenu into returned SubMenu
166                        parseMenu(parser, attrs, subMenu);
167                    } else {
168                        lookingForEndOfUnknownTag = true;
169                        unknownTagName = tagName;
170                    }
171                    break;
172
173                case XmlPullParser.END_TAG:
174                    tagName = parser.getName();
175                    if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
176                        lookingForEndOfUnknownTag = false;
177                        unknownTagName = null;
178                    } else if (tagName.equals(XML_GROUP)) {
179                        menuState.resetGroup();
180                    } else if (tagName.equals(XML_ITEM)) {
181                        // Add the item if it hasn't been added (if the item was
182                        // a submenu, it would have been added already)
183                        if (!menuState.hasAddedItem()) {
184                            if (menuState.itemActionProvider != null &&
185                                    menuState.itemActionProvider.hasSubMenu()) {
186                                menuState.addSubMenuItem();
187                            } else {
188                                menuState.addItem();
189                            }
190                        }
191                    } else if (tagName.equals(XML_MENU)) {
192                        reachedEndOfMenu = true;
193                    }
194                    break;
195
196                case XmlPullParser.END_DOCUMENT:
197                    throw new RuntimeException("Unexpected end of document");
198            }
199
200            eventType = parser.next();
201        }
202    }
203
204    private static class InflatedOnMenuItemClickListener
205            implements MenuItem.OnMenuItemClickListener {
206        private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };
207
208        private Object mRealOwner;
209        private Method mMethod;
210
211        public InflatedOnMenuItemClickListener(Object realOwner, String methodName) {
212            mRealOwner = realOwner;
213            Class<?> c = realOwner.getClass();
214            try {
215                mMethod = c.getMethod(methodName, PARAM_TYPES);
216            } catch (Exception e) {
217                InflateException ex = new InflateException(
218                        "Couldn't resolve menu item onClick handler " + methodName +
219                        " in class " + c.getName());
220                ex.initCause(e);
221                throw ex;
222            }
223        }
224
225        public boolean onMenuItemClick(MenuItem item) {
226            try {
227                if (mMethod.getReturnType() == Boolean.TYPE) {
228                    return (Boolean) mMethod.invoke(mRealOwner, item);
229                } else {
230                    mMethod.invoke(mRealOwner, item);
231                    return true;
232                }
233            } catch (Exception e) {
234                throw new RuntimeException(e);
235            }
236        }
237    }
238
239    /**
240     * State for the current menu.
241     * <p>
242     * Groups can not be nested unless there is another menu (which will have
243     * its state class).
244     */
245    private class MenuState {
246        private Menu menu;
247
248        /*
249         * Group state is set on items as they are added, allowing an item to
250         * override its group state. (As opposed to set on items at the group end tag.)
251         */
252        private int groupId;
253        private int groupCategory;
254        private int groupOrder;
255        private int groupCheckable;
256        private boolean groupVisible;
257        private boolean groupEnabled;
258
259        private boolean itemAdded;
260        private int itemId;
261        private int itemCategoryOrder;
262        private CharSequence itemTitle;
263        private CharSequence itemTitleCondensed;
264        private int itemIconResId;
265        private char itemAlphabeticShortcut;
266        private char itemNumericShortcut;
267        /**
268         * Sync to attrs.xml enum:
269         * - 0: none
270         * - 1: all
271         * - 2: exclusive
272         */
273        private int itemCheckable;
274        private boolean itemChecked;
275        private boolean itemVisible;
276        private boolean itemEnabled;
277
278        /**
279         * Sync to attrs.xml enum, values in MenuItem:
280         * - 0: never
281         * - 1: ifRoom
282         * - 2: always
283         * - -1: Safe sentinel for "no value".
284         */
285        private int itemShowAsAction;
286
287        private int itemActionViewLayout;
288        private String itemActionViewClassName;
289        private String itemActionProviderClassName;
290
291        private String itemListenerMethodName;
292
293        private ActionProvider itemActionProvider;
294
295        private static final int defaultGroupId = NO_ID;
296        private static final int defaultItemId = NO_ID;
297        private static final int defaultItemCategory = 0;
298        private static final int defaultItemOrder = 0;
299        private static final int defaultItemCheckable = 0;
300        private static final boolean defaultItemChecked = false;
301        private static final boolean defaultItemVisible = true;
302        private static final boolean defaultItemEnabled = true;
303
304        public MenuState(final Menu menu) {
305            this.menu = menu;
306
307            resetGroup();
308        }
309
310        public void resetGroup() {
311            groupId = defaultGroupId;
312            groupCategory = defaultItemCategory;
313            groupOrder = defaultItemOrder;
314            groupCheckable = defaultItemCheckable;
315            groupVisible = defaultItemVisible;
316            groupEnabled = defaultItemEnabled;
317        }
318
319        /**
320         * Called when the parser is pointing to a group tag.
321         */
322        public void readGroup(AttributeSet attrs) {
323            TypedArray a = mContext.obtainStyledAttributes(attrs,
324                    com.android.internal.R.styleable.MenuGroup);
325
326            groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId);
327            groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory);
328            groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder);
329            groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable);
330            groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible);
331            groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled);
332
333            a.recycle();
334        }
335
336        /**
337         * Called when the parser is pointing to an item tag.
338         */
339        public void readItem(AttributeSet attrs) {
340            TypedArray a = mContext.obtainStyledAttributes(attrs,
341                    com.android.internal.R.styleable.MenuItem);
342
343            // Inherit attributes from the group as default value
344            itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);
345            final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);
346            final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder);
347            itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
348            itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title);
349            itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed);
350            itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0);
351            itemAlphabeticShortcut =
352                    getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut));
353            itemNumericShortcut =
354                    getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut));
355            if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) {
356                // Item has attribute checkable, use it
357                itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0;
358            } else {
359                // Item does not have attribute, use the group's (group can have one more state
360                // for checkable that represents the exclusive checkable)
361                itemCheckable = groupCheckable;
362            }
363            itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked);
364            itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible);
365            itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled);
366            itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1);
367            itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick);
368            itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0);
369            itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass);
370            itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass);
371
372            final boolean hasActionProvider = itemActionProviderClassName != null;
373            if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
374                itemActionProvider = newInstance(itemActionProviderClassName,
375                            ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
376                            mActionProviderConstructorArguments);
377            } else {
378                if (hasActionProvider) {
379                    Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
380                            + " Action view already specified.");
381                }
382                itemActionProvider = null;
383            }
384
385            a.recycle();
386
387            itemAdded = false;
388        }
389
390        private char getShortcut(String shortcutString) {
391            if (shortcutString == null) {
392                return 0;
393            } else {
394                return shortcutString.charAt(0);
395            }
396        }
397
398        private void setItem(MenuItem item) {
399            item.setChecked(itemChecked)
400                .setVisible(itemVisible)
401                .setEnabled(itemEnabled)
402                .setCheckable(itemCheckable >= 1)
403                .setTitleCondensed(itemTitleCondensed)
404                .setIcon(itemIconResId)
405                .setAlphabeticShortcut(itemAlphabeticShortcut)
406                .setNumericShortcut(itemNumericShortcut);
407
408            if (itemShowAsAction >= 0) {
409                item.setShowAsAction(itemShowAsAction);
410            }
411
412            if (itemListenerMethodName != null) {
413                if (mContext.isRestricted()) {
414                    throw new IllegalStateException("The android:onClick attribute cannot "
415                            + "be used within a restricted context");
416                }
417                item.setOnMenuItemClickListener(
418                        new InflatedOnMenuItemClickListener(mRealOwner, itemListenerMethodName));
419            }
420
421            if (item instanceof MenuItemImpl) {
422                MenuItemImpl impl = (MenuItemImpl) item;
423                if (itemCheckable >= 2) {
424                    impl.setExclusiveCheckable(true);
425                }
426            }
427
428            boolean actionViewSpecified = false;
429            if (itemActionViewClassName != null) {
430                View actionView = (View) newInstance(itemActionViewClassName,
431                        ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
432                item.setActionView(actionView);
433                actionViewSpecified = true;
434            }
435            if (itemActionViewLayout > 0) {
436                if (!actionViewSpecified) {
437                    item.setActionView(itemActionViewLayout);
438                    actionViewSpecified = true;
439                } else {
440                    Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."
441                            + " Action view already specified.");
442                }
443            }
444            if (itemActionProvider != null) {
445                item.setActionProvider(itemActionProvider);
446            }
447        }
448
449        public void addItem() {
450            itemAdded = true;
451            setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
452        }
453
454        public SubMenu addSubMenuItem() {
455            itemAdded = true;
456            SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
457            setItem(subMenu.getItem());
458            return subMenu;
459        }
460
461        public boolean hasAddedItem() {
462            return itemAdded;
463        }
464
465        @SuppressWarnings("unchecked")
466        private <T> T newInstance(String className, Class<?>[] constructorSignature,
467                Object[] arguments) {
468            try {
469                Class<?> clazz = mContext.getClassLoader().loadClass(className);
470                Constructor<?> constructor = clazz.getConstructor(constructorSignature);
471                return (T) constructor.newInstance(arguments);
472            } catch (Exception e) {
473                Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
474            }
475            return null;
476        }
477    }
478}
479