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