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 */ 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 */ 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 */ 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 */ 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 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 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 304 public MenuState(final Menu menu) { 305 this.menu = menu; 306 307 resetGroup(); 308 } 309 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 */ 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 */ 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 390 private char getShortcut(String shortcutString) { 391 if (shortcutString == null) { 392 return 0; 393 } else { 394 return shortcutString.charAt(0); 395 } 396 } 397 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 449 public void addItem() { 450 itemAdded = true; 451 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); 452 } 453 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 461 public boolean hasAddedItem() { 462 return itemAdded; 463 } 464 465 @SuppressWarnings("unchecked") 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