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