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