1/* 2 * Copyright (C) 2013 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 17package android.support.v7.internal.view; 18 19import org.xmlpull.v1.XmlPullParser; 20import org.xmlpull.v1.XmlPullParserException; 21 22import android.app.Activity; 23import android.content.Context; 24import android.content.res.TypedArray; 25import android.content.res.XmlResourceParser; 26import android.support.v4.internal.view.SupportMenu; 27import android.support.v4.view.ActionProvider; 28import android.support.v4.view.MenuItemCompat; 29import android.support.v7.appcompat.R; 30import android.support.v7.internal.view.menu.MenuItemImpl; 31import android.support.v7.internal.view.menu.MenuItemWrapperICS; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.util.Xml; 35import android.view.InflateException; 36import android.view.Menu; 37import android.view.MenuInflater; 38import android.view.MenuItem; 39import android.view.SubMenu; 40import android.view.View; 41 42import java.io.IOException; 43import java.lang.reflect.Constructor; 44import java.lang.reflect.Method; 45 46/** 47 * This class is used to instantiate menu XML files into Menu objects. 48 * <p> 49 * For performance reasons, menu inflation relies heavily on pre-processing of 50 * XML files that is done at build time. Therefore, it is not currently possible 51 * to use SupportMenuInflater with an XmlPullParser over a plain XML file at runtime; 52 * it only works with an XmlPullParser returned from a compiled resource (R. 53 * <em>something</em> file.) 54 * 55 * @hide 56 */ 57public class SupportMenuInflater extends MenuInflater { 58 private static final String LOG_TAG = "SupportMenuInflater"; 59 60 /** Menu tag name in XML. */ 61 private static final String XML_MENU = "menu"; 62 63 /** Group tag name in XML. */ 64 private static final String XML_GROUP = "group"; 65 66 /** Item tag name in XML. */ 67 private static final String XML_ITEM = "item"; 68 69 private static final int NO_ID = 0; 70 71 private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class}; 72 73 private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = 74 ACTION_VIEW_CONSTRUCTOR_SIGNATURE; 75 76 private final Object[] mActionViewConstructorArguments; 77 78 private final Object[] mActionProviderConstructorArguments; 79 80 private Context mContext; 81 private Object mRealOwner; 82 83 /** 84 * Constructs a menu inflater. 85 * 86 * @see Activity#getMenuInflater() 87 */ 88 public SupportMenuInflater(Context context) { 89 super(context); 90 mContext = context; 91 mRealOwner = context; 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 */ 105 @Override 106 public void inflate(int menuRes, Menu menu) { 107 // If we're not dealing with a SupportMenu instance, let super handle 108 if (!(menu instanceof SupportMenu)) { 109 super.inflate(menuRes, menu); 110 return; 111 } 112 113 XmlResourceParser parser = null; 114 try { 115 parser = mContext.getResources().getLayout(menuRes); 116 AttributeSet attrs = Xml.asAttributeSet(parser); 117 118 parseMenu(parser, attrs, menu); 119 } catch (XmlPullParserException e) { 120 throw new InflateException("Error inflating menu XML", e); 121 } catch (IOException e) { 122 throw new InflateException("Error inflating menu XML", e); 123 } finally { 124 if (parser != null) parser.close(); 125 } 126 } 127 128 /** 129 * Called internally to fill the given menu. If a sub menu is seen, it will 130 * call this recursively. 131 */ 132 private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) 133 throws XmlPullParserException, IOException { 134 MenuState menuState = new MenuState(menu); 135 136 int eventType = parser.getEventType(); 137 String tagName; 138 boolean lookingForEndOfUnknownTag = false; 139 String unknownTagName = null; 140 141 // This loop will skip to the menu start tag 142 do { 143 if (eventType == XmlPullParser.START_TAG) { 144 tagName = parser.getName(); 145 if (tagName.equals(XML_MENU)) { 146 // Go to next tag 147 eventType = parser.next(); 148 break; 149 } 150 151 throw new RuntimeException("Expecting menu, got " + tagName); 152 } 153 eventType = parser.next(); 154 } while (eventType != XmlPullParser.END_DOCUMENT); 155 156 boolean reachedEndOfMenu = false; 157 while (!reachedEndOfMenu) { 158 switch (eventType) { 159 case XmlPullParser.START_TAG: 160 if (lookingForEndOfUnknownTag) { 161 break; 162 } 163 164 tagName = parser.getName(); 165 if (tagName.equals(XML_GROUP)) { 166 menuState.readGroup(attrs); 167 } else if (tagName.equals(XML_ITEM)) { 168 menuState.readItem(attrs); 169 } else if (tagName.equals(XML_MENU)) { 170 // A menu start tag denotes a submenu for an item 171 SubMenu subMenu = menuState.addSubMenuItem(); 172 173 // Parse the submenu into returned SubMenu 174 parseMenu(parser, attrs, subMenu); 175 } else { 176 lookingForEndOfUnknownTag = true; 177 unknownTagName = tagName; 178 } 179 break; 180 181 case XmlPullParser.END_TAG: 182 tagName = parser.getName(); 183 if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { 184 lookingForEndOfUnknownTag = false; 185 unknownTagName = null; 186 } else if (tagName.equals(XML_GROUP)) { 187 menuState.resetGroup(); 188 } else if (tagName.equals(XML_ITEM)) { 189 // Add the item if it hasn't been added (if the item was 190 // a submenu, it would have been added already) 191 if (!menuState.hasAddedItem()) { 192 if (menuState.itemActionProvider != null && 193 menuState.itemActionProvider.hasSubMenu()) { 194 menuState.addSubMenuItem(); 195 } else { 196 menuState.addItem(); 197 } 198 } 199 } else if (tagName.equals(XML_MENU)) { 200 reachedEndOfMenu = true; 201 } 202 break; 203 204 case XmlPullParser.END_DOCUMENT: 205 throw new RuntimeException("Unexpected end of document"); 206 } 207 208 eventType = parser.next(); 209 } 210 } 211 212 private static class InflatedOnMenuItemClickListener 213 implements MenuItem.OnMenuItemClickListener { 214 private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class }; 215 216 private Object mRealOwner; 217 private Method mMethod; 218 219 public InflatedOnMenuItemClickListener(Object realOwner, String methodName) { 220 mRealOwner = realOwner; 221 Class<?> c = realOwner.getClass(); 222 try { 223 mMethod = c.getMethod(methodName, PARAM_TYPES); 224 } catch (Exception e) { 225 InflateException ex = new InflateException( 226 "Couldn't resolve menu item onClick handler " + methodName + 227 " in class " + c.getName()); 228 ex.initCause(e); 229 throw ex; 230 } 231 } 232 233 public boolean onMenuItemClick(MenuItem item) { 234 try { 235 if (mMethod.getReturnType() == Boolean.TYPE) { 236 return (Boolean) mMethod.invoke(mRealOwner, item); 237 } else { 238 mMethod.invoke(mRealOwner, item); 239 return true; 240 } 241 } catch (Exception e) { 242 throw new RuntimeException(e); 243 } 244 } 245 } 246 247 /** 248 * State for the current menu. 249 * <p> 250 * Groups can not be nested unless there is another menu (which will have 251 * its state class). 252 */ 253 private class MenuState { 254 private Menu menu; 255 256 /* 257 * Group state is set on items as they are added, allowing an item to 258 * override its group state. (As opposed to set on items at the group end tag.) 259 */ 260 private int groupId; 261 private int groupCategory; 262 private int groupOrder; 263 private int groupCheckable; 264 private boolean groupVisible; 265 private boolean groupEnabled; 266 267 private boolean itemAdded; 268 private int itemId; 269 private int itemCategoryOrder; 270 private CharSequence itemTitle; 271 private CharSequence itemTitleCondensed; 272 private int itemIconResId; 273 private char itemAlphabeticShortcut; 274 private char itemNumericShortcut; 275 /** 276 * Sync to attrs.xml enum: 277 * - 0: none 278 * - 1: all 279 * - 2: exclusive 280 */ 281 private int itemCheckable; 282 private boolean itemChecked; 283 private boolean itemVisible; 284 private boolean itemEnabled; 285 286 /** 287 * Sync to attrs.xml enum, values in MenuItem: 288 * - 0: never 289 * - 1: ifRoom 290 * - 2: always 291 * - -1: Safe sentinel for "no value". 292 */ 293 private int itemShowAsAction; 294 295 private int itemActionViewLayout; 296 private String itemActionViewClassName; 297 private String itemActionProviderClassName; 298 299 private String itemListenerMethodName; 300 301 private ActionProvider itemActionProvider; 302 303 private static final int defaultGroupId = NO_ID; 304 private static final int defaultItemId = NO_ID; 305 private static final int defaultItemCategory = 0; 306 private static final int defaultItemOrder = 0; 307 private static final int defaultItemCheckable = 0; 308 private static final boolean defaultItemChecked = false; 309 private static final boolean defaultItemVisible = true; 310 private static final boolean defaultItemEnabled = true; 311 312 public MenuState(final Menu menu) { 313 this.menu = menu; 314 315 resetGroup(); 316 } 317 318 public void resetGroup() { 319 groupId = defaultGroupId; 320 groupCategory = defaultItemCategory; 321 groupOrder = defaultItemOrder; 322 groupCheckable = defaultItemCheckable; 323 groupVisible = defaultItemVisible; 324 groupEnabled = defaultItemEnabled; 325 } 326 327 /** 328 * Called when the parser is pointing to a group tag. 329 */ 330 public void readGroup(AttributeSet attrs) { 331 TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuGroup); 332 333 groupId = a.getResourceId(R.styleable.MenuGroup_android_id, defaultGroupId); 334 groupCategory = a.getInt( 335 R.styleable.MenuGroup_android_menuCategory, defaultItemCategory); 336 groupOrder = a.getInt(R.styleable.MenuGroup_android_orderInCategory, defaultItemOrder); 337 groupCheckable = a.getInt( 338 R.styleable.MenuGroup_android_checkableBehavior, defaultItemCheckable); 339 groupVisible = a.getBoolean(R.styleable.MenuGroup_android_visible, defaultItemVisible); 340 groupEnabled = a.getBoolean(R.styleable.MenuGroup_android_enabled, defaultItemEnabled); 341 342 a.recycle(); 343 } 344 345 /** 346 * Called when the parser is pointing to an item tag. 347 */ 348 public void readItem(AttributeSet attrs) { 349 TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuItem); 350 351 // Inherit attributes from the group as default value 352 itemId = a.getResourceId(R.styleable.MenuItem_android_id, defaultItemId); 353 final int category = a.getInt(R.styleable.MenuItem_android_menuCategory, groupCategory); 354 final int order = a.getInt(R.styleable.MenuItem_android_orderInCategory, groupOrder); 355 itemCategoryOrder = (category & SupportMenu.CATEGORY_MASK) | 356 (order & SupportMenu.USER_MASK); 357 itemTitle = a.getText(R.styleable.MenuItem_android_title); 358 itemTitleCondensed = a.getText(R.styleable.MenuItem_android_titleCondensed); 359 itemIconResId = a.getResourceId(R.styleable.MenuItem_android_icon, 0); 360 itemAlphabeticShortcut = 361 getShortcut(a.getString(R.styleable.MenuItem_android_alphabeticShortcut)); 362 itemNumericShortcut = 363 getShortcut(a.getString(R.styleable.MenuItem_android_numericShortcut)); 364 if (a.hasValue(R.styleable.MenuItem_android_checkable)) { 365 // Item has attribute checkable, use it 366 itemCheckable = a.getBoolean(R.styleable.MenuItem_android_checkable, false) ? 1 : 0; 367 } else { 368 // Item does not have attribute, use the group's (group can have one more state 369 // for checkable that represents the exclusive checkable) 370 itemCheckable = groupCheckable; 371 } 372 itemChecked = a.getBoolean(R.styleable.MenuItem_android_checked, defaultItemChecked); 373 itemVisible = a.getBoolean(R.styleable.MenuItem_android_visible, groupVisible); 374 itemEnabled = a.getBoolean(R.styleable.MenuItem_android_enabled, groupEnabled); 375 itemShowAsAction = a.getInt(R.styleable.MenuItem_showAsAction, -1); 376 itemListenerMethodName = a.getString(R.styleable.MenuItem_android_onClick); 377 itemActionViewLayout = a.getResourceId(R.styleable.MenuItem_actionLayout, 0); 378 itemActionViewClassName = a.getString(R.styleable.MenuItem_actionViewClass); 379 itemActionProviderClassName = a.getString(R.styleable.MenuItem_actionProviderClass); 380 381 final boolean hasActionProvider = itemActionProviderClassName != null; 382 if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) { 383 itemActionProvider = newInstance(itemActionProviderClassName, 384 ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE, 385 mActionProviderConstructorArguments); 386 } else { 387 if (hasActionProvider) { 388 Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'." 389 + " Action view already specified."); 390 } 391 itemActionProvider = null; 392 } 393 394 a.recycle(); 395 396 itemAdded = false; 397 } 398 399 private char getShortcut(String shortcutString) { 400 if (shortcutString == null) { 401 return 0; 402 } else { 403 return shortcutString.charAt(0); 404 } 405 } 406 407 private void setItem(MenuItem item) { 408 item.setChecked(itemChecked) 409 .setVisible(itemVisible) 410 .setEnabled(itemEnabled) 411 .setCheckable(itemCheckable >= 1) 412 .setTitleCondensed(itemTitleCondensed) 413 .setIcon(itemIconResId) 414 .setAlphabeticShortcut(itemAlphabeticShortcut) 415 .setNumericShortcut(itemNumericShortcut); 416 417 if (itemShowAsAction >= 0) { 418 MenuItemCompat.setShowAsAction(item, itemShowAsAction); 419 } 420 421 if (itemListenerMethodName != null) { 422 if (mContext.isRestricted()) { 423 throw new IllegalStateException("The android:onClick attribute cannot " 424 + "be used within a restricted context"); 425 } 426 item.setOnMenuItemClickListener( 427 new InflatedOnMenuItemClickListener(mRealOwner, itemListenerMethodName)); 428 } 429 430 final MenuItemImpl impl = item instanceof MenuItemImpl ? (MenuItemImpl) item : null; 431 if (itemCheckable >= 2) { 432 if (item instanceof MenuItemImpl) { 433 ((MenuItemImpl) item).setExclusiveCheckable(true); 434 } else if (item instanceof MenuItemWrapperICS) { 435 ((MenuItemWrapperICS) item).setExclusiveCheckable(true); 436 } 437 } 438 439 boolean actionViewSpecified = false; 440 if (itemActionViewClassName != null) { 441 View actionView = (View) newInstance(itemActionViewClassName, 442 ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments); 443 MenuItemCompat.setActionView(item, actionView); 444 actionViewSpecified = true; 445 } 446 if (itemActionViewLayout > 0) { 447 if (!actionViewSpecified) { 448 MenuItemCompat.setActionView(item, itemActionViewLayout); 449 actionViewSpecified = true; 450 } else { 451 Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'." 452 + " Action view already specified."); 453 } 454 } 455 if (itemActionProvider != null) { 456 MenuItemCompat.setActionProvider(item, itemActionProvider); 457 } 458 } 459 460 public void addItem() { 461 itemAdded = true; 462 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); 463 } 464 465 public SubMenu addSubMenuItem() { 466 itemAdded = true; 467 SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); 468 setItem(subMenu.getItem()); 469 return subMenu; 470 } 471 472 public boolean hasAddedItem() { 473 return itemAdded; 474 } 475 476 @SuppressWarnings("unchecked") 477 private <T> T newInstance(String className, Class<?>[] constructorSignature, 478 Object[] arguments) { 479 try { 480 Class<?> clazz = mContext.getClassLoader().loadClass(className); 481 Constructor<?> constructor = clazz.getConstructor(constructorSignature); 482 return (T) constructor.newInstance(arguments); 483 } catch (Exception e) { 484 Log.w(LOG_TAG, "Cannot instantiate class: " + className, e); 485 } 486 return null; 487 } 488 } 489} 490