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 69 /** 70 * Constructs a menu inflater. 71 * 72 * @see Activity#getMenuInflater() 73 */ 74 public MenuInflater(Context context) { 75 mContext = context; 76 mActionViewConstructorArguments = new Object[] {context}; 77 mActionProviderConstructorArguments = mActionViewConstructorArguments; 78 } 79 80 /** 81 * Inflate a menu hierarchy from the specified XML resource. Throws 82 * {@link InflateException} if there is an error. 83 * 84 * @param menuRes Resource ID for an XML layout resource to load (e.g., 85 * <code>R.menu.main_activity</code>) 86 * @param menu The Menu to inflate into. The items and submenus will be 87 * added to this Menu. 88 */ 89 public void inflate(int menuRes, Menu menu) { 90 XmlResourceParser parser = null; 91 try { 92 parser = mContext.getResources().getLayout(menuRes); 93 AttributeSet attrs = Xml.asAttributeSet(parser); 94 95 parseMenu(parser, attrs, menu); 96 } catch (XmlPullParserException e) { 97 throw new InflateException("Error inflating menu XML", e); 98 } catch (IOException e) { 99 throw new InflateException("Error inflating menu XML", e); 100 } finally { 101 if (parser != null) parser.close(); 102 } 103 } 104 105 /** 106 * Called internally to fill the given menu. If a sub menu is seen, it will 107 * call this recursively. 108 */ 109 private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) 110 throws XmlPullParserException, IOException { 111 MenuState menuState = new MenuState(menu); 112 113 int eventType = parser.getEventType(); 114 String tagName; 115 boolean lookingForEndOfUnknownTag = false; 116 String unknownTagName = null; 117 118 // This loop will skip to the menu start tag 119 do { 120 if (eventType == XmlPullParser.START_TAG) { 121 tagName = parser.getName(); 122 if (tagName.equals(XML_MENU)) { 123 // Go to next tag 124 eventType = parser.next(); 125 break; 126 } 127 128 throw new RuntimeException("Expecting menu, got " + tagName); 129 } 130 eventType = parser.next(); 131 } while (eventType != XmlPullParser.END_DOCUMENT); 132 133 boolean reachedEndOfMenu = false; 134 while (!reachedEndOfMenu) { 135 switch (eventType) { 136 case XmlPullParser.START_TAG: 137 if (lookingForEndOfUnknownTag) { 138 break; 139 } 140 141 tagName = parser.getName(); 142 if (tagName.equals(XML_GROUP)) { 143 menuState.readGroup(attrs); 144 } else if (tagName.equals(XML_ITEM)) { 145 menuState.readItem(attrs); 146 } else if (tagName.equals(XML_MENU)) { 147 // A menu start tag denotes a submenu for an item 148 SubMenu subMenu = menuState.addSubMenuItem(); 149 150 // Parse the submenu into returned SubMenu 151 parseMenu(parser, attrs, subMenu); 152 } else { 153 lookingForEndOfUnknownTag = true; 154 unknownTagName = tagName; 155 } 156 break; 157 158 case XmlPullParser.END_TAG: 159 tagName = parser.getName(); 160 if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { 161 lookingForEndOfUnknownTag = false; 162 unknownTagName = null; 163 } else if (tagName.equals(XML_GROUP)) { 164 menuState.resetGroup(); 165 } else if (tagName.equals(XML_ITEM)) { 166 // Add the item if it hasn't been added (if the item was 167 // a submenu, it would have been added already) 168 if (!menuState.hasAddedItem()) { 169 if (menuState.itemActionProvider != null && 170 menuState.itemActionProvider.hasSubMenu()) { 171 menuState.addSubMenuItem(); 172 } else { 173 menuState.addItem(); 174 } 175 } 176 } else if (tagName.equals(XML_MENU)) { 177 reachedEndOfMenu = true; 178 } 179 break; 180 181 case XmlPullParser.END_DOCUMENT: 182 throw new RuntimeException("Unexpected end of document"); 183 } 184 185 eventType = parser.next(); 186 } 187 } 188 189 private static class InflatedOnMenuItemClickListener 190 implements MenuItem.OnMenuItemClickListener { 191 private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class }; 192 193 private Context mContext; 194 private Method mMethod; 195 196 public InflatedOnMenuItemClickListener(Context context, String methodName) { 197 mContext = context; 198 Class<?> c = context.getClass(); 199 try { 200 mMethod = c.getMethod(methodName, PARAM_TYPES); 201 } catch (Exception e) { 202 InflateException ex = new InflateException( 203 "Couldn't resolve menu item onClick handler " + methodName + 204 " in class " + c.getName()); 205 ex.initCause(e); 206 throw ex; 207 } 208 } 209 210 public boolean onMenuItemClick(MenuItem item) { 211 try { 212 if (mMethod.getReturnType() == Boolean.TYPE) { 213 return (Boolean) mMethod.invoke(mContext, item); 214 } else { 215 mMethod.invoke(mContext, item); 216 return true; 217 } 218 } catch (Exception e) { 219 throw new RuntimeException(e); 220 } 221 } 222 } 223 224 /** 225 * State for the current menu. 226 * <p> 227 * Groups can not be nested unless there is another menu (which will have 228 * its state class). 229 */ 230 private class MenuState { 231 private Menu menu; 232 233 /* 234 * Group state is set on items as they are added, allowing an item to 235 * override its group state. (As opposed to set on items at the group end tag.) 236 */ 237 private int groupId; 238 private int groupCategory; 239 private int groupOrder; 240 private int groupCheckable; 241 private boolean groupVisible; 242 private boolean groupEnabled; 243 244 private boolean itemAdded; 245 private int itemId; 246 private int itemCategoryOrder; 247 private CharSequence itemTitle; 248 private CharSequence itemTitleCondensed; 249 private int itemIconResId; 250 private char itemAlphabeticShortcut; 251 private char itemNumericShortcut; 252 /** 253 * Sync to attrs.xml enum: 254 * - 0: none 255 * - 1: all 256 * - 2: exclusive 257 */ 258 private int itemCheckable; 259 private boolean itemChecked; 260 private boolean itemVisible; 261 private boolean itemEnabled; 262 263 /** 264 * Sync to attrs.xml enum, values in MenuItem: 265 * - 0: never 266 * - 1: ifRoom 267 * - 2: always 268 * - -1: Safe sentinel for "no value". 269 */ 270 private int itemShowAsAction; 271 272 private int itemActionViewLayout; 273 private String itemActionViewClassName; 274 private String itemActionProviderClassName; 275 276 private String itemListenerMethodName; 277 278 private ActionProvider itemActionProvider; 279 280 private static final int defaultGroupId = NO_ID; 281 private static final int defaultItemId = NO_ID; 282 private static final int defaultItemCategory = 0; 283 private static final int defaultItemOrder = 0; 284 private static final int defaultItemCheckable = 0; 285 private static final boolean defaultItemChecked = false; 286 private static final boolean defaultItemVisible = true; 287 private static final boolean defaultItemEnabled = true; 288 289 public MenuState(final Menu menu) { 290 this.menu = menu; 291 292 resetGroup(); 293 } 294 295 public void resetGroup() { 296 groupId = defaultGroupId; 297 groupCategory = defaultItemCategory; 298 groupOrder = defaultItemOrder; 299 groupCheckable = defaultItemCheckable; 300 groupVisible = defaultItemVisible; 301 groupEnabled = defaultItemEnabled; 302 } 303 304 /** 305 * Called when the parser is pointing to a group tag. 306 */ 307 public void readGroup(AttributeSet attrs) { 308 TypedArray a = mContext.obtainStyledAttributes(attrs, 309 com.android.internal.R.styleable.MenuGroup); 310 311 groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId); 312 groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory); 313 groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder); 314 groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable); 315 groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible); 316 groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled); 317 318 a.recycle(); 319 } 320 321 /** 322 * Called when the parser is pointing to an item tag. 323 */ 324 public void readItem(AttributeSet attrs) { 325 TypedArray a = mContext.obtainStyledAttributes(attrs, 326 com.android.internal.R.styleable.MenuItem); 327 328 // Inherit attributes from the group as default value 329 itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId); 330 final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory); 331 final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder); 332 itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK); 333 itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title); 334 itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed); 335 itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0); 336 itemAlphabeticShortcut = 337 getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut)); 338 itemNumericShortcut = 339 getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut)); 340 if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) { 341 // Item has attribute checkable, use it 342 itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0; 343 } else { 344 // Item does not have attribute, use the group's (group can have one more state 345 // for checkable that represents the exclusive checkable) 346 itemCheckable = groupCheckable; 347 } 348 itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); 349 itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); 350 itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); 351 itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1); 352 itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); 353 itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0); 354 itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass); 355 itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass); 356 357 final boolean hasActionProvider = itemActionProviderClassName != null; 358 if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) { 359 itemActionProvider = newInstance(itemActionProviderClassName, 360 ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE, 361 mActionProviderConstructorArguments); 362 } else { 363 if (hasActionProvider) { 364 Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'." 365 + " Action view already specified."); 366 } 367 itemActionProvider = null; 368 } 369 370 a.recycle(); 371 372 itemAdded = false; 373 } 374 375 private char getShortcut(String shortcutString) { 376 if (shortcutString == null) { 377 return 0; 378 } else { 379 return shortcutString.charAt(0); 380 } 381 } 382 383 private void setItem(MenuItem item) { 384 item.setChecked(itemChecked) 385 .setVisible(itemVisible) 386 .setEnabled(itemEnabled) 387 .setCheckable(itemCheckable >= 1) 388 .setTitleCondensed(itemTitleCondensed) 389 .setIcon(itemIconResId) 390 .setAlphabeticShortcut(itemAlphabeticShortcut) 391 .setNumericShortcut(itemNumericShortcut); 392 393 if (itemShowAsAction >= 0) { 394 item.setShowAsAction(itemShowAsAction); 395 } 396 397 if (itemListenerMethodName != null) { 398 if (mContext.isRestricted()) { 399 throw new IllegalStateException("The android:onClick attribute cannot " 400 + "be used within a restricted context"); 401 } 402 item.setOnMenuItemClickListener( 403 new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName)); 404 } 405 406 if (item instanceof MenuItemImpl) { 407 MenuItemImpl impl = (MenuItemImpl) item; 408 if (itemCheckable >= 2) { 409 impl.setExclusiveCheckable(true); 410 } 411 } 412 413 boolean actionViewSpecified = false; 414 if (itemActionViewClassName != null) { 415 View actionView = (View) newInstance(itemActionViewClassName, 416 ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments); 417 item.setActionView(actionView); 418 actionViewSpecified = true; 419 } 420 if (itemActionViewLayout > 0) { 421 if (!actionViewSpecified) { 422 item.setActionView(itemActionViewLayout); 423 actionViewSpecified = true; 424 } else { 425 Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'." 426 + " Action view already specified."); 427 } 428 } 429 if (itemActionProvider != null) { 430 item.setActionProvider(itemActionProvider); 431 } 432 } 433 434 public void addItem() { 435 itemAdded = true; 436 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); 437 } 438 439 public SubMenu addSubMenuItem() { 440 itemAdded = true; 441 SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); 442 setItem(subMenu.getItem()); 443 return subMenu; 444 } 445 446 public boolean hasAddedItem() { 447 return itemAdded; 448 } 449 450 @SuppressWarnings("unchecked") 451 private <T> T newInstance(String className, Class<?>[] constructorSignature, 452 Object[] arguments) { 453 try { 454 Class<?> clazz = mContext.getClassLoader().loadClass(className); 455 Constructor<?> constructor = clazz.getConstructor(constructorSignature); 456 return (T) constructor.newInstance(arguments); 457 } catch (Exception e) { 458 Log.w(LOG_TAG, "Cannot instantiate class: " + className, e); 459 } 460 return null; 461 } 462 } 463 } 464