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