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