1 /* 2 * Copyright (C) 2008 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.SdkConstants; 20 import com.android.ide.common.rendering.api.LayoutLog; 21 import com.android.ide.common.rendering.api.LayoutlibCallback; 22 import com.android.ide.common.rendering.api.MergeCookie; 23 import com.android.ide.common.rendering.api.ResourceNamespace; 24 import com.android.ide.common.rendering.api.ResourceReference; 25 import com.android.ide.common.rendering.api.ResourceValue; 26 import com.android.layoutlib.bridge.Bridge; 27 import com.android.layoutlib.bridge.BridgeConstants; 28 import com.android.layoutlib.bridge.MockView; 29 import com.android.layoutlib.bridge.android.BridgeContext; 30 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 31 import com.android.layoutlib.bridge.android.UnresolvedResourceValue; 32 import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil; 33 import com.android.layoutlib.bridge.android.support.RecyclerViewUtil; 34 import com.android.layoutlib.bridge.impl.ParserFactory; 35 import com.android.layoutlib.bridge.util.ReflectionUtils; 36 import com.android.resources.ResourceType; 37 import com.android.tools.layoutlib.annotations.NotNull; 38 import com.android.tools.layoutlib.annotations.Nullable; 39 40 import org.xmlpull.v1.XmlPullParser; 41 42 import android.annotation.NonNull; 43 import android.content.Context; 44 import android.content.res.TypedArray; 45 import android.graphics.drawable.Animatable; 46 import android.graphics.drawable.Drawable; 47 import android.util.AttributeSet; 48 import android.util.ResolvingAttributeSet; 49 import android.view.View.OnAttachStateChangeListener; 50 import android.widget.ImageView; 51 import android.widget.NumberPicker; 52 53 import java.lang.reflect.Constructor; 54 import java.lang.reflect.InvocationTargetException; 55 import java.lang.reflect.Method; 56 import java.util.HashMap; 57 import java.util.Map; 58 import java.util.function.BiFunction; 59 60 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext; 61 62 /** 63 * Custom implementation of {@link LayoutInflater} to handle custom views. 64 */ 65 public final class BridgeInflater extends LayoutInflater { 66 private static final String INFLATER_CLASS_ATTR_NAME = "viewInflaterClass"; 67 private static final ResourceReference RES_AUTO_INFLATER_CLASS_ATTR = 68 ResourceReference.attr(ResourceNamespace.RES_AUTO, INFLATER_CLASS_ATTR_NAME); 69 private static final ResourceReference LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR = 70 ResourceReference.attr(ResourceNamespace.APPCOMPAT_LEGACY, INFLATER_CLASS_ATTR_NAME); 71 private static final ResourceReference ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR = 72 ResourceReference.attr(ResourceNamespace.APPCOMPAT, INFLATER_CLASS_ATTR_NAME); 73 private static final String LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME = 74 "android.support.v7.app.AppCompatViewInflater"; 75 private static final String ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME = 76 "androidx.appcompat.app.AppCompatViewInflater"; 77 private final LayoutlibCallback mLayoutlibCallback; 78 79 private boolean mIsInMerge = false; 80 private ResourceReference mResourceReference; 81 private Map<View, String> mOpenDrawerLayouts; 82 83 // Keep in sync with the same value in LayoutInflater. 84 private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme }; 85 86 /** 87 * List of class prefixes which are tried first by default. 88 * <p/> 89 * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater. 90 */ 91 private static final String[] sClassPrefixList = { 92 "android.widget.", 93 "android.webkit.", 94 "android.app." 95 }; 96 private BiFunction<String, AttributeSet, View> mCustomInflater; 97 getClassPrefixList()98 public static String[] getClassPrefixList() { 99 return sClassPrefixList; 100 } 101 BridgeInflater(LayoutInflater original, Context newContext)102 private BridgeInflater(LayoutInflater original, Context newContext) { 103 super(original, newContext); 104 newContext = getBaseContext(newContext); 105 mLayoutlibCallback = (newContext instanceof BridgeContext) ? 106 ((BridgeContext) newContext).getLayoutlibCallback() : 107 null; 108 } 109 110 /** 111 * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object. 112 * 113 * @param context The Android application context. 114 * @param layoutlibCallback the {@link LayoutlibCallback} object. 115 */ BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback)116 public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) { 117 super(context); 118 mLayoutlibCallback = layoutlibCallback; 119 mConstructorArgs[0] = context; 120 } 121 122 @Override onCreateView(String name, AttributeSet attrs)123 public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { 124 View view = createViewFromCustomInflater(name, attrs); 125 126 if (view == null) { 127 try { 128 // First try to find a class using the default Android prefixes 129 for (String prefix : sClassPrefixList) { 130 try { 131 view = createView(name, prefix, attrs); 132 if (view != null) { 133 break; 134 } 135 } catch (ClassNotFoundException e) { 136 // Ignore. We'll try again using the base class below. 137 } 138 } 139 140 // Next try using the parent loader. This will most likely only work for 141 // fully-qualified class names. 142 try { 143 if (view == null) { 144 view = super.onCreateView(name, attrs); 145 } 146 } catch (ClassNotFoundException e) { 147 // Ignore. We'll try again using the custom view loader below. 148 } 149 150 // Finally try again using the custom view loader 151 if (view == null) { 152 view = loadCustomView(name, attrs); 153 } 154 } catch (InflateException e) { 155 // Don't catch the InflateException below as that results in hiding the real cause. 156 throw e; 157 } catch (Exception e) { 158 // Wrap the real exception in a ClassNotFoundException, so that the calling method 159 // can deal with it. 160 throw new ClassNotFoundException("onCreateView", e); 161 } 162 } 163 164 setupViewInContext(view, attrs); 165 166 return view; 167 } 168 169 /** 170 * Finds the createView method in the given customInflaterClass. Since createView is 171 * currently package protected, it will show in the declared class so we iterate up the 172 * hierarchy and return the first instance we find. 173 * The returned method will be accessible. 174 */ 175 @NotNull getCreateViewMethod(Class<?> customInflaterClass)176 private static Method getCreateViewMethod(Class<?> customInflaterClass) throws NoSuchMethodException { 177 Class<?> current = customInflaterClass; 178 do { 179 try { 180 Method method = current.getDeclaredMethod("createView", View.class, String.class, 181 Context.class, AttributeSet.class, boolean.class, boolean.class, 182 boolean.class, boolean.class); 183 method.setAccessible(true); 184 return method; 185 } catch (NoSuchMethodException ignore) { 186 } 187 current = current.getSuperclass(); 188 } while (current != null && current != Object.class); 189 190 throw new NoSuchMethodException(); 191 } 192 193 /** 194 * Finds the custom inflater class. If it's defined in the theme, we'll use that one (if the 195 * class does not exist, null is returned). 196 * If {@code viewInflaterClass} is not defined in the theme, we'll try to instantiate 197 * {@code android.support.v7.app.AppCompatViewInflater} 198 */ 199 @Nullable findCustomInflater(@otNull BridgeContext bc, @NotNull LayoutlibCallback layoutlibCallback)200 private static Class<?> findCustomInflater(@NotNull BridgeContext bc, 201 @NotNull LayoutlibCallback layoutlibCallback) { 202 ResourceReference attrRef; 203 if (layoutlibCallback.isResourceNamespacingRequired()) { 204 if (layoutlibCallback.hasLegacyAppCompat()) { 205 attrRef = LEGACY_APPCOMPAT_INFLATER_CLASS_ATTR; 206 } else if (layoutlibCallback.hasAndroidXAppCompat()) { 207 attrRef = ANDROIDX_APPCOMPAT_INFLATER_CLASS_ATTR; 208 } else { 209 return null; 210 } 211 } else { 212 attrRef = RES_AUTO_INFLATER_CLASS_ATTR; 213 } 214 ResourceValue value = bc.getRenderResources().findItemInTheme(attrRef); 215 String inflaterName = value != null ? value.getValue() : null; 216 217 if (inflaterName != null) { 218 try { 219 return layoutlibCallback.findClass(inflaterName); 220 } catch (ClassNotFoundException ignore) { 221 } 222 223 // viewInflaterClass was defined but we couldn't find the class. 224 } else if (bc.isAppCompatTheme()) { 225 // Older versions of AppCompat do not define the viewInflaterClass so try to get it 226 // manually. 227 try { 228 if (layoutlibCallback.hasLegacyAppCompat()) { 229 return layoutlibCallback.findClass(LEGACY_DEFAULT_APPCOMPAT_INFLATER_NAME); 230 } else if (layoutlibCallback.hasAndroidXAppCompat()) { 231 return layoutlibCallback.findClass(ANDROIDX_DEFAULT_APPCOMPAT_INFLATER_NAME); 232 } 233 } catch (ClassNotFoundException ignore) { 234 } 235 } 236 237 return null; 238 } 239 240 /** 241 * Checks if there is a custom inflater and, when present, tries to instantiate the view 242 * using it. 243 */ 244 @Nullable createViewFromCustomInflater(@otNull String name, @NotNull AttributeSet attrs)245 private View createViewFromCustomInflater(@NotNull String name, @NotNull AttributeSet attrs) { 246 if (mCustomInflater == null) { 247 Context context = getContext(); 248 context = getBaseContext(context); 249 if (context instanceof BridgeContext) { 250 BridgeContext bc = (BridgeContext) context; 251 Class<?> inflaterClass = findCustomInflater(bc, mLayoutlibCallback); 252 253 if (inflaterClass != null) { 254 try { 255 Constructor<?> constructor = inflaterClass.getDeclaredConstructor(); 256 constructor.setAccessible(true); 257 Object inflater = constructor.newInstance(); 258 Method method = getCreateViewMethod(inflaterClass); 259 Context finalContext = context; 260 mCustomInflater = (viewName, attributeSet) -> { 261 try { 262 return (View) method.invoke(inflater, null, viewName, finalContext, 263 attributeSet, 264 false, 265 false /*readAndroidTheme*/, // No need after L 266 true /*readAppTheme*/, 267 true /*wrapContext*/); 268 } catch (IllegalAccessException | InvocationTargetException e) { 269 assert false : "Call to createView failed"; 270 } 271 return null; 272 }; 273 } catch (InvocationTargetException | IllegalAccessException | 274 NoSuchMethodException | InstantiationException ignore) { 275 } 276 } 277 } 278 279 if (mCustomInflater == null) { 280 // There is no custom inflater. We'll create a nop custom inflater to avoid the 281 // penalty of trying to instantiate again 282 mCustomInflater = (s, attributeSet) -> null; 283 } 284 } 285 286 return mCustomInflater.apply(name, attrs); 287 } 288 289 @Override createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)290 public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, 291 boolean ignoreThemeAttr) { 292 View view = null; 293 if (name.equals("view")) { 294 // This is usually done by the superclass but this allows us catching the error and 295 // reporting something useful. 296 name = attrs.getAttributeValue(null, "class"); 297 298 if (name == null) { 299 Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Unable to inflate view tag without " + 300 "class attribute", null); 301 // We weren't able to resolve the view so we just pass a mock View to be able to 302 // continue rendering. 303 view = new MockView(context, attrs); 304 ((MockView) view).setText("view"); 305 } 306 } 307 308 try { 309 if (view == null) { 310 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr); 311 } 312 } catch (InflateException e) { 313 // Creation of ContextThemeWrapper code is same as in the super method. 314 // Apply a theme wrapper, if allowed and one is specified. 315 if (!ignoreThemeAttr) { 316 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); 317 final int themeResId = ta.getResourceId(0, 0); 318 if (themeResId != 0) { 319 context = new ContextThemeWrapper(context, themeResId); 320 } 321 ta.recycle(); 322 } 323 if (!(e.getCause() instanceof ClassNotFoundException)) { 324 // There is some unknown inflation exception in inflating a View that was found. 325 view = new MockView(context, attrs); 326 ((MockView) view).setText(name); 327 Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null); 328 } else { 329 final Object lastContext = mConstructorArgs[0]; 330 mConstructorArgs[0] = context; 331 // try to load the class from using the custom view loader 332 try { 333 view = loadCustomView(name, attrs); 334 } catch (Exception e2) { 335 // Wrap the real exception in an InflateException so that the calling 336 // method can deal with it. 337 InflateException exception = new InflateException(); 338 if (!e2.getClass().equals(ClassNotFoundException.class)) { 339 exception.initCause(e2); 340 } else { 341 exception.initCause(e); 342 } 343 throw exception; 344 } finally { 345 mConstructorArgs[0] = lastContext; 346 } 347 } 348 } 349 350 setupViewInContext(view, attrs); 351 352 return view; 353 } 354 355 @Override inflate(int resource, ViewGroup root)356 public View inflate(int resource, ViewGroup root) { 357 Context context = getContext(); 358 context = getBaseContext(context); 359 if (context instanceof BridgeContext) { 360 BridgeContext bridgeContext = (BridgeContext)context; 361 362 ResourceValue value = null; 363 364 ResourceReference layoutInfo = Bridge.resolveResourceId(resource); 365 if (layoutInfo == null) { 366 layoutInfo = mLayoutlibCallback.resolveResourceId(resource); 367 368 } 369 if (layoutInfo != null) { 370 value = bridgeContext.getRenderResources().getResolvedResource(layoutInfo); 371 } 372 373 if (value != null) { 374 String path = value.getValue(); 375 try { 376 XmlPullParser parser = ParserFactory.create(path, true); 377 if (parser == null) { 378 return null; 379 } 380 381 BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( 382 parser, bridgeContext, value.getNamespace()); 383 384 return inflate(bridgeParser, root); 385 } catch (Exception e) { 386 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 387 "Failed to parse file " + path, e, null); 388 389 return null; 390 } 391 } 392 } 393 return null; 394 } 395 396 /** 397 * Instantiates the given view name and returns the instance. If the view doesn't exist, a 398 * MockView or null might be returned. 399 * @param name the custom view name 400 * @param attrs the {@link AttributeSet} to be passed to the view constructor 401 * @param silent if true, errors while loading the view won't be reported and, if the view 402 * doesn't exist, null will be returned. 403 */ loadCustomView(String name, AttributeSet attrs, boolean silent)404 private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception { 405 if (mLayoutlibCallback != null) { 406 // first get the classname in case it's not the node name 407 if (name.equals("view")) { 408 name = attrs.getAttributeValue(null, "class"); 409 if (name == null) { 410 return null; 411 } 412 } 413 414 mConstructorArgs[1] = attrs; 415 416 Object customView = silent ? 417 mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs) 418 : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs); 419 420 if (customView instanceof View) { 421 return (View)customView; 422 } 423 } 424 425 return null; 426 } 427 loadCustomView(String name, AttributeSet attrs)428 private View loadCustomView(String name, AttributeSet attrs) throws Exception { 429 return loadCustomView(name, attrs, false); 430 } 431 setupViewInContext(View view, AttributeSet attrs)432 private void setupViewInContext(View view, AttributeSet attrs) { 433 Context context = getContext(); 434 context = getBaseContext(context); 435 if (!(context instanceof BridgeContext)) { 436 return; 437 } 438 439 BridgeContext bc = (BridgeContext) context; 440 // get the view key 441 Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge); 442 if (viewKey != null) { 443 bc.addViewKey(view, viewKey); 444 } 445 String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX"); 446 if (scrollPosX != null && scrollPosX.endsWith("px")) { 447 int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2)); 448 bc.setScrollXPos(view, value); 449 } 450 String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY"); 451 if (scrollPosY != null && scrollPosY.endsWith("px")) { 452 int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2)); 453 bc.setScrollYPos(view, value); 454 } 455 if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) { 456 int resourceId = 0; 457 int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI, 458 BridgeConstants.ATTR_ITEM_COUNT, -1); 459 if (attrs instanceof ResolvingAttributeSet) { 460 ResourceValue attrListItemValue = 461 ((ResolvingAttributeSet) attrs).getResolvedAttributeValue( 462 BridgeConstants.NS_TOOLS_URI, BridgeConstants.ATTR_LIST_ITEM); 463 if (attrListItemValue != null) { 464 resourceId = bc.getResourceId(attrListItemValue.asReference(), 0); 465 } 466 } 467 RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue); 468 } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) { 469 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 470 BridgeConstants.ATTR_OPEN_DRAWER); 471 if (attrVal != null) { 472 getDrawerLayoutMap().put(view, attrVal); 473 } 474 } 475 else if (view instanceof NumberPicker) { 476 NumberPicker numberPicker = (NumberPicker) view; 477 String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue"); 478 if (minValue != null) { 479 numberPicker.setMinValue(Integer.parseInt(minValue)); 480 } 481 String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue"); 482 if (maxValue != null) { 483 numberPicker.setMaxValue(Integer.parseInt(maxValue)); 484 } 485 } 486 else if (view instanceof ImageView) { 487 ImageView img = (ImageView) view; 488 Drawable drawable = img.getDrawable(); 489 if (drawable instanceof Animatable) { 490 if (!((Animatable) drawable).isRunning()) { 491 ((Animatable) drawable).start(); 492 } 493 } 494 } 495 else if (view instanceof ViewStub) { 496 // By default, ViewStub will be set to GONE and won't be inflate. If the XML has the 497 // tools:visibility attribute we'll workaround that behavior. 498 String visibility = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, 499 SdkConstants.ATTR_VISIBILITY); 500 501 boolean isVisible = "visible".equals(visibility); 502 if (isVisible || "invisible".equals(visibility)) { 503 // We can not inflate the view until is attached to its parent so we need to delay 504 // the setVisible call until after that happens. 505 final int visibilityValue = isVisible ? View.VISIBLE : View.INVISIBLE; 506 view.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 507 @Override 508 public void onViewAttachedToWindow(View v) { 509 v.removeOnAttachStateChangeListener(this); 510 view.setVisibility(visibilityValue); 511 } 512 513 @Override 514 public void onViewDetachedFromWindow(View v) {} 515 }); 516 } 517 } 518 519 } 520 setIsInMerge(boolean isInMerge)521 public void setIsInMerge(boolean isInMerge) { 522 mIsInMerge = isInMerge; 523 } 524 setResourceReference(ResourceReference reference)525 public void setResourceReference(ResourceReference reference) { 526 mResourceReference = reference; 527 } 528 529 @Override cloneInContext(Context newContext)530 public LayoutInflater cloneInContext(Context newContext) { 531 return new BridgeInflater(this, newContext); 532 } 533 getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, ResourceReference resourceReference, boolean isInMerge)534 /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc, 535 ResourceReference resourceReference, boolean isInMerge) { 536 537 if (!(attrs instanceof BridgeXmlBlockParser)) { 538 return null; 539 } 540 BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs); 541 542 // get the view key 543 Object viewKey = parser.getViewCookie(); 544 545 if (viewKey == null) { 546 int currentDepth = parser.getDepth(); 547 548 // test whether we are in an included file or in a adapter binding view. 549 BridgeXmlBlockParser previousParser = bc.getPreviousParser(); 550 if (previousParser != null) { 551 // looks like we are inside an embedded layout. 552 // only apply the cookie of the calling node (<include>) if we are at the 553 // top level of the embedded layout. If there is a merge tag, then 554 // skip it and look for the 2nd level 555 int testDepth = isInMerge ? 2 : 1; 556 if (currentDepth == testDepth) { 557 viewKey = previousParser.getViewCookie(); 558 // if we are in a merge, wrap the cookie in a MergeCookie. 559 if (viewKey != null && isInMerge) { 560 viewKey = new MergeCookie(viewKey); 561 } 562 } 563 } else if (resourceReference != null && currentDepth == 1) { 564 // else if there's a resource reference, this means we are in an adapter 565 // binding case. Set the resource ref as the view cookie only for the top 566 // level view. 567 viewKey = resourceReference; 568 } 569 } 570 571 return viewKey; 572 } 573 postInflateProcess(View view)574 public void postInflateProcess(View view) { 575 if (mOpenDrawerLayouts != null) { 576 String gravity = mOpenDrawerLayouts.get(view); 577 if (gravity != null) { 578 DrawerLayoutUtil.openDrawer(view, gravity); 579 } 580 mOpenDrawerLayouts.remove(view); 581 } 582 } 583 584 @NonNull getDrawerLayoutMap()585 private Map<View, String> getDrawerLayoutMap() { 586 if (mOpenDrawerLayouts == null) { 587 mOpenDrawerLayouts = new HashMap<>(4); 588 } 589 return mOpenDrawerLayouts; 590 } 591 onDoneInflation()592 public void onDoneInflation() { 593 if (mOpenDrawerLayouts != null) { 594 mOpenDrawerLayouts.clear(); 595 } 596 } 597 } 598