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.ContextWrapper; 25import android.content.res.TypedArray; 26import android.content.res.XmlResourceParser; 27import android.support.v4.internal.view.SupportMenu; 28import android.support.v4.view.ActionProvider; 29import android.support.v4.view.MenuItemCompat; 30import android.support.v7.appcompat.R; 31import android.support.v7.internal.view.menu.MenuItemImpl; 32import android.support.v7.internal.view.menu.MenuItemWrapperICS; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.util.Xml; 36import android.view.InflateException; 37import android.view.Menu; 38import android.view.MenuInflater; 39import android.view.MenuItem; 40import android.view.SubMenu; 41import android.view.View; 42 43import java.io.IOException; 44import java.lang.reflect.Constructor; 45import java.lang.reflect.Method; 46 47/** 48 * This class is used to instantiate menu XML files into Menu objects. 49 * <p> 50 * For performance reasons, menu inflation relies heavily on pre-processing of 51 * XML files that is done at build time. Therefore, it is not currently possible 52 * to use SupportMenuInflater with an XmlPullParser over a plain XML file at runtime; 53 * it only works with an XmlPullParser returned from a compiled resource (R. 54 * <em>something</em> file.) 55 * 56 * @hide 57 */ 58public class SupportMenuInflater extends MenuInflater { 59 private static final String LOG_TAG = "SupportMenuInflater"; 60 61 /** Menu tag name in XML. */ 62 private static final String XML_MENU = "menu"; 63 64 /** Group tag name in XML. */ 65 private static final String XML_GROUP = "group"; 66 67 /** Item tag name in XML. */ 68 private static final String XML_ITEM = "item"; 69 70 private static final int NO_ID = 0; 71 72 private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class}; 73 74 private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = 75 ACTION_VIEW_CONSTRUCTOR_SIGNATURE; 76 77 private final Object[] mActionViewConstructorArguments; 78 79 private final Object[] mActionProviderConstructorArguments; 80 81 private Context mContext; 82 private Object mRealOwner; 83 84 /** 85 * Constructs a menu inflater. 86 * 87 * @see Activity#getMenuInflater() 88 */ 89 public SupportMenuInflater(Context context) { 90 super(context); 91 mContext = 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 Object getRealOwner() { 213 if (mRealOwner == null) { 214 mRealOwner = findRealOwner(mContext); 215 } 216 return mRealOwner; 217 } 218 219 private Object findRealOwner(Object owner) { 220 if (owner instanceof Activity) { 221 return owner; 222 } 223 if (owner instanceof ContextWrapper) { 224 return findRealOwner(((ContextWrapper) owner).getBaseContext()); 225 } 226 return owner; 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 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 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 264 /** 265 * State for the current menu. 266 * <p> 267 * Groups can not be nested unless there is another menu (which will have 268 * its state class). 269 */ 270 private class MenuState { 271 private Menu menu; 272 273 /* 274 * Group state is set on items as they are added, allowing an item to 275 * override its group state. (As opposed to set on items at the group end tag.) 276 */ 277 private int groupId; 278 private int groupCategory; 279 private int groupOrder; 280 private int groupCheckable; 281 private boolean groupVisible; 282 private boolean groupEnabled; 283 284 private boolean itemAdded; 285 private int itemId; 286 private int itemCategoryOrder; 287 private CharSequence itemTitle; 288 private CharSequence itemTitleCondensed; 289 private int itemIconResId; 290 private char itemAlphabeticShortcut; 291 private char itemNumericShortcut; 292 /** 293 * Sync to attrs.xml enum: 294 * - 0: none 295 * - 1: all 296 * - 2: exclusive 297 */ 298 private int itemCheckable; 299 private boolean itemChecked; 300 private boolean itemVisible; 301 private boolean itemEnabled; 302 303 /** 304 * Sync to attrs.xml enum, values in MenuItem: 305 * - 0: never 306 * - 1: ifRoom 307 * - 2: always 308 * - -1: Safe sentinel for "no value". 309 */ 310 private int itemShowAsAction; 311 312 private int itemActionViewLayout; 313 private String itemActionViewClassName; 314 private String itemActionProviderClassName; 315 316 private String itemListenerMethodName; 317 318 private ActionProvider itemActionProvider; 319 320 private static final int defaultGroupId = NO_ID; 321 private static final int defaultItemId = NO_ID; 322 private static final int defaultItemCategory = 0; 323 private static final int defaultItemOrder = 0; 324 private static final int defaultItemCheckable = 0; 325 private static final boolean defaultItemChecked = false; 326 private static final boolean defaultItemVisible = true; 327 private static final boolean defaultItemEnabled = true; 328 329 public MenuState(final Menu menu) { 330 this.menu = menu; 331 332 resetGroup(); 333 } 334 335 public void resetGroup() { 336 groupId = defaultGroupId; 337 groupCategory = defaultItemCategory; 338 groupOrder = defaultItemOrder; 339 groupCheckable = defaultItemCheckable; 340 groupVisible = defaultItemVisible; 341 groupEnabled = defaultItemEnabled; 342 } 343 344 /** 345 * Called when the parser is pointing to a group tag. 346 */ 347 public void readGroup(AttributeSet attrs) { 348 TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuGroup); 349 350 groupId = a.getResourceId(R.styleable.MenuGroup_android_id, defaultGroupId); 351 groupCategory = a.getInt( 352 R.styleable.MenuGroup_android_menuCategory, defaultItemCategory); 353 groupOrder = a.getInt(R.styleable.MenuGroup_android_orderInCategory, defaultItemOrder); 354 groupCheckable = a.getInt( 355 R.styleable.MenuGroup_android_checkableBehavior, defaultItemCheckable); 356 groupVisible = a.getBoolean(R.styleable.MenuGroup_android_visible, defaultItemVisible); 357 groupEnabled = a.getBoolean(R.styleable.MenuGroup_android_enabled, defaultItemEnabled); 358 359 a.recycle(); 360 } 361 362 /** 363 * Called when the parser is pointing to an item tag. 364 */ 365 public void readItem(AttributeSet attrs) { 366 TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MenuItem); 367 368 // Inherit attributes from the group as default value 369 itemId = a.getResourceId(R.styleable.MenuItem_android_id, defaultItemId); 370 final int category = a.getInt(R.styleable.MenuItem_android_menuCategory, groupCategory); 371 final int order = a.getInt(R.styleable.MenuItem_android_orderInCategory, groupOrder); 372 itemCategoryOrder = (category & SupportMenu.CATEGORY_MASK) | 373 (order & SupportMenu.USER_MASK); 374 itemTitle = a.getText(R.styleable.MenuItem_android_title); 375 itemTitleCondensed = a.getText(R.styleable.MenuItem_android_titleCondensed); 376 itemIconResId = a.getResourceId(R.styleable.MenuItem_android_icon, 0); 377 itemAlphabeticShortcut = 378 getShortcut(a.getString(R.styleable.MenuItem_android_alphabeticShortcut)); 379 itemNumericShortcut = 380 getShortcut(a.getString(R.styleable.MenuItem_android_numericShortcut)); 381 if (a.hasValue(R.styleable.MenuItem_android_checkable)) { 382 // Item has attribute checkable, use it 383 itemCheckable = a.getBoolean(R.styleable.MenuItem_android_checkable, false) ? 1 : 0; 384 } else { 385 // Item does not have attribute, use the group's (group can have one more state 386 // for checkable that represents the exclusive checkable) 387 itemCheckable = groupCheckable; 388 } 389 itemChecked = a.getBoolean(R.styleable.MenuItem_android_checked, defaultItemChecked); 390 itemVisible = a.getBoolean(R.styleable.MenuItem_android_visible, groupVisible); 391 itemEnabled = a.getBoolean(R.styleable.MenuItem_android_enabled, groupEnabled); 392 itemShowAsAction = a.getInt(R.styleable.MenuItem_showAsAction, -1); 393 itemListenerMethodName = a.getString(R.styleable.MenuItem_android_onClick); 394 itemActionViewLayout = a.getResourceId(R.styleable.MenuItem_actionLayout, 0); 395 itemActionViewClassName = a.getString(R.styleable.MenuItem_actionViewClass); 396 itemActionProviderClassName = a.getString(R.styleable.MenuItem_actionProviderClass); 397 398 final boolean hasActionProvider = itemActionProviderClassName != null; 399 if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) { 400 itemActionProvider = newInstance(itemActionProviderClassName, 401 ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE, 402 mActionProviderConstructorArguments); 403 } else { 404 if (hasActionProvider) { 405 Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'." 406 + " Action view already specified."); 407 } 408 itemActionProvider = null; 409 } 410 411 a.recycle(); 412 413 itemAdded = false; 414 } 415 416 private char getShortcut(String shortcutString) { 417 if (shortcutString == null) { 418 return 0; 419 } else { 420 return shortcutString.charAt(0); 421 } 422 } 423 424 private void setItem(MenuItem item) { 425 item.setChecked(itemChecked) 426 .setVisible(itemVisible) 427 .setEnabled(itemEnabled) 428 .setCheckable(itemCheckable >= 1) 429 .setTitleCondensed(itemTitleCondensed) 430 .setIcon(itemIconResId) 431 .setAlphabeticShortcut(itemAlphabeticShortcut) 432 .setNumericShortcut(itemNumericShortcut); 433 434 if (itemShowAsAction >= 0) { 435 MenuItemCompat.setShowAsAction(item, itemShowAsAction); 436 } 437 438 if (itemListenerMethodName != null) { 439 if (mContext.isRestricted()) { 440 throw new IllegalStateException("The android:onClick attribute cannot " 441 + "be used within a restricted context"); 442 } 443 item.setOnMenuItemClickListener( 444 new InflatedOnMenuItemClickListener(getRealOwner(), itemListenerMethodName)); 445 } 446 447 final MenuItemImpl impl = item instanceof MenuItemImpl ? (MenuItemImpl) item : null; 448 if (itemCheckable >= 2) { 449 if (item instanceof MenuItemImpl) { 450 ((MenuItemImpl) item).setExclusiveCheckable(true); 451 } else if (item instanceof MenuItemWrapperICS) { 452 ((MenuItemWrapperICS) item).setExclusiveCheckable(true); 453 } 454 } 455 456 boolean actionViewSpecified = false; 457 if (itemActionViewClassName != null) { 458 View actionView = (View) newInstance(itemActionViewClassName, 459 ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments); 460 MenuItemCompat.setActionView(item, actionView); 461 actionViewSpecified = true; 462 } 463 if (itemActionViewLayout > 0) { 464 if (!actionViewSpecified) { 465 MenuItemCompat.setActionView(item, itemActionViewLayout); 466 actionViewSpecified = true; 467 } else { 468 Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'." 469 + " Action view already specified."); 470 } 471 } 472 if (itemActionProvider != null) { 473 MenuItemCompat.setActionProvider(item, itemActionProvider); 474 } 475 } 476 477 public void addItem() { 478 itemAdded = true; 479 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); 480 } 481 482 public SubMenu addSubMenuItem() { 483 itemAdded = true; 484 SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); 485 setItem(subMenu.getItem()); 486 return subMenu; 487 } 488 489 public boolean hasAddedItem() { 490 return itemAdded; 491 } 492 493 @SuppressWarnings("unchecked") 494 private <T> T newInstance(String className, Class<?>[] constructorSignature, 495 Object[] arguments) { 496 try { 497 Class<?> clazz = mContext.getClassLoader().loadClass(className); 498 Constructor<?> constructor = clazz.getConstructor(constructorSignature); 499 constructor.setAccessible(true); 500 return (T) constructor.newInstance(arguments); 501 } catch (Exception e) { 502 Log.w(LOG_TAG, "Cannot instantiate class: " + className, e); 503 } 504 return null; 505 } 506 } 507} 508