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