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