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