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