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 com.android.internal.view.menu.MenuItemImpl;
20 
21 import org.xmlpull.v1.XmlPullParser;
22 import org.xmlpull.v1.XmlPullParserException;
23 
24 import android.annotation.MenuRes;
25 import android.app.Activity;
26 import android.content.Context;
27 import android.content.ContextWrapper;
28 import android.content.res.TypedArray;
29 import android.content.res.XmlResourceParser;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.util.Xml;
33 
34 import java.io.IOException;
35 import java.lang.reflect.Constructor;
36 import 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  */
47 public 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      */
MenuInflater(Context context)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      */
MenuInflater(Context context, Object realOwner)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      */
inflate(@enuRes int menuRes, Menu menu)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      */
parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)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      */
registerMenu(@uppressWarnings"unused") MenuItem item, @SuppressWarnings("unused") AttributeSet set)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      */
registerMenu(@uppressWarnings"unused") SubMenu subMenu, @SuppressWarnings("unused") AttributeSet set)220     private void registerMenu(@SuppressWarnings("unused") SubMenu subMenu,
221             @SuppressWarnings("unused") AttributeSet set) {
222     }
223 
224     // Needed by layoutlib.
getContext()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 
InflatedOnMenuItemClickListener(Object realOwner, String methodName)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 
onMenuItemClick(MenuItem item)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 
getRealOwner()264     private Object getRealOwner() {
265         if (mRealOwner == null) {
266             mRealOwner = findRealOwner(mContext);
267         }
268         return mRealOwner;
269     }
270 
findRealOwner(Object owner)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 
MenuState(final Menu menu)346         public MenuState(final Menu menu) {
347             this.menu = menu;
348 
349             resetGroup();
350         }
351 
resetGroup()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          */
readGroup(AttributeSet attrs)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          */
readItem(AttributeSet attrs)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 
getShortcut(String shortcutString)432         private char getShortcut(String shortcutString) {
433             if (shortcutString == null) {
434                 return 0;
435             } else {
436                 return shortcutString.charAt(0);
437             }
438         }
439 
setItem(MenuItem item)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 
addItem()491         public MenuItem addItem() {
492             itemAdded = true;
493             MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle);
494             setItem(item);
495             return item;
496         }
497 
addSubMenuItem()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 
hasAddedItem()505         public boolean hasAddedItem() {
506             return itemAdded;
507         }
508 
509         @SuppressWarnings("unchecked")
newInstance(String className, Class<?>[] constructorSignature, Object[] arguments)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