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