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