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 com.android.layoutlib.bridge; 18 19 import com.android.ide.common.rendering.api.Capability; 20 import com.android.ide.common.rendering.api.DrawableParams; 21 import com.android.ide.common.rendering.api.Features; 22 import com.android.ide.common.rendering.api.LayoutLog; 23 import com.android.ide.common.rendering.api.RenderSession; 24 import com.android.ide.common.rendering.api.Result; 25 import com.android.ide.common.rendering.api.Result.Status; 26 import com.android.ide.common.rendering.api.SessionParams; 27 import com.android.layoutlib.bridge.android.RenderParamsFlags; 28 import com.android.layoutlib.bridge.impl.RenderDrawable; 29 import com.android.layoutlib.bridge.impl.RenderSessionImpl; 30 import com.android.layoutlib.bridge.util.DynamicIdMap; 31 import com.android.ninepatch.NinePatchChunk; 32 import com.android.resources.ResourceType; 33 import com.android.tools.layoutlib.create.MethodAdapter; 34 import com.android.tools.layoutlib.create.OverrideMethod; 35 import com.android.util.Pair; 36 37 import android.annotation.NonNull; 38 import android.content.res.BridgeAssetManager; 39 import android.graphics.Bitmap; 40 import android.graphics.FontFamily_Delegate; 41 import android.graphics.Typeface; 42 import android.graphics.Typeface_Delegate; 43 import android.icu.util.ULocale; 44 import android.os.Looper; 45 import android.os.Looper_Accessor; 46 import android.view.View; 47 import android.view.ViewGroup; 48 import android.view.ViewParent; 49 50 import java.io.File; 51 import java.lang.ref.SoftReference; 52 import java.lang.reflect.Field; 53 import java.lang.reflect.Modifier; 54 import java.util.Arrays; 55 import java.util.EnumMap; 56 import java.util.EnumSet; 57 import java.util.HashMap; 58 import java.util.Map; 59 import java.util.WeakHashMap; 60 import java.util.concurrent.locks.ReentrantLock; 61 62 import libcore.io.MemoryMappedFile_Delegate; 63 64 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; 65 66 /** 67 * Main entry point of the LayoutLib Bridge. 68 * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call 69 * {@link #createSession(SessionParams)} 70 */ 71 public final class Bridge extends com.android.ide.common.rendering.api.Bridge { 72 73 private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left"; 74 75 public static class StaticMethodNotImplementedException extends RuntimeException { 76 private static final long serialVersionUID = 1L; 77 StaticMethodNotImplementedException(String msg)78 public StaticMethodNotImplementedException(String msg) { 79 super(msg); 80 } 81 } 82 83 /** 84 * Lock to ensure only one rendering/inflating happens at a time. 85 * This is due to some singleton in the Android framework. 86 */ 87 private final static ReentrantLock sLock = new ReentrantLock(); 88 89 /** 90 * Maps from id to resource type/name. This is for com.android.internal.R 91 */ 92 @SuppressWarnings("deprecation") 93 private final static Map<Integer, Pair<ResourceType, String>> sRMap = new HashMap<>(); 94 95 /** 96 * Reverse map compared to sRMap, resource type -> (resource name -> id). 97 * This is for com.android.internal.R. 98 */ 99 private final static Map<ResourceType, Map<String, Integer>> sRevRMap = new EnumMap<>(ResourceType.class); 100 101 // framework resources are defined as 0x01XX#### where XX is the resource type (layout, 102 // drawable, etc...). Using FF as the type allows for 255 resource types before we get a 103 // collision which should be fine. 104 private final static int DYNAMIC_ID_SEED_START = 0x01ff0000; 105 private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START); 106 107 private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache = 108 new WeakHashMap<>(); 109 private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache = 110 new WeakHashMap<>(); 111 112 private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache = new HashMap<>(); 113 private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache = 114 new HashMap<>(); 115 116 private static Map<String, Map<String, Integer>> sEnumValueMap; 117 private static Map<String, String> sPlatformProperties; 118 119 /** 120 * A default log than prints to stdout/stderr. 121 */ 122 private final static LayoutLog sDefaultLog = new LayoutLog() { 123 @Override 124 public void error(String tag, String message, Object data) { 125 System.err.println(message); 126 } 127 128 @Override 129 public void error(String tag, String message, Throwable throwable, Object data) { 130 System.err.println(message); 131 } 132 133 @Override 134 public void warning(String tag, String message, Object data) { 135 System.out.println(message); 136 } 137 }; 138 139 /** 140 * Current log. 141 */ 142 private static LayoutLog sCurrentLog = sDefaultLog; 143 144 private static final int LAST_SUPPORTED_FEATURE = Features.THEME_PREVIEW_NAVIGATION_BAR; 145 146 @Override getApiLevel()147 public int getApiLevel() { 148 return com.android.ide.common.rendering.api.Bridge.API_CURRENT; 149 } 150 151 @SuppressWarnings("deprecation") 152 @Override 153 @Deprecated getCapabilities()154 public EnumSet<Capability> getCapabilities() { 155 // The Capability class is deprecated and frozen. All Capabilities enumerated there are 156 // supported by this version of LayoutLibrary. So, it's safe to use EnumSet.allOf() 157 return EnumSet.allOf(Capability.class); 158 } 159 160 @Override supports(int feature)161 public boolean supports(int feature) { 162 return feature <= LAST_SUPPORTED_FEATURE; 163 } 164 165 @Override init(Map<String,String> platformProperties, File fontLocation, Map<String, Map<String, Integer>> enumValueMap, LayoutLog log)166 public boolean init(Map<String,String> platformProperties, 167 File fontLocation, 168 Map<String, Map<String, Integer>> enumValueMap, 169 LayoutLog log) { 170 sPlatformProperties = platformProperties; 171 sEnumValueMap = enumValueMap; 172 173 BridgeAssetManager.initSystem(); 174 175 // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener 176 // on static (native) methods which prints the signature on the console and 177 // throws an exception. 178 // This is useful when testing the rendering in ADT to identify static native 179 // methods that are ignored -- layoutlib_create makes them returns 0/false/null 180 // which is generally OK yet might be a problem, so this is how you'd find out. 181 // 182 // Currently layoutlib_create only overrides static native method. 183 // Static non-natives are not overridden and thus do not get here. 184 final String debug = System.getenv("DEBUG_LAYOUT"); 185 if (debug != null && !debug.equals("0") && !debug.equals("false")) { 186 187 OverrideMethod.setDefaultListener(new MethodAdapter() { 188 @Override 189 public void onInvokeV(String signature, boolean isNative, Object caller) { 190 sDefaultLog.error(null, "Missing Stub: " + signature + 191 (isNative ? " (native)" : ""), null /*data*/); 192 193 if (debug.equalsIgnoreCase("throw")) { 194 // Throwing this exception doesn't seem that useful. It breaks 195 // the layout editor yet doesn't display anything meaningful to the 196 // user. Having the error in the console is just as useful. We'll 197 // throw it only if the environment variable is "throw" or "THROW". 198 throw new StaticMethodNotImplementedException(signature); 199 } 200 } 201 }); 202 } 203 204 // load the fonts. 205 FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath()); 206 MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile()); 207 208 // now parse com.android.internal.R (and only this one as android.R is a subset of 209 // the internal version), and put the content in the maps. 210 try { 211 Class<?> r = com.android.internal.R.class; 212 // Parse the styleable class first, since it may contribute to attr values. 213 parseStyleable(); 214 215 for (Class<?> inner : r.getDeclaredClasses()) { 216 if (inner == com.android.internal.R.styleable.class) { 217 // Already handled the styleable case. Not skipping attr, as there may be attrs 218 // that are not referenced from styleables. 219 continue; 220 } 221 String resTypeName = inner.getSimpleName(); 222 ResourceType resType = ResourceType.getEnum(resTypeName); 223 if (resType != null) { 224 Map<String, Integer> fullMap = null; 225 switch (resType) { 226 case ATTR: 227 fullMap = sRevRMap.get(ResourceType.ATTR); 228 break; 229 case STRING: 230 case STYLE: 231 // Slightly less than thousand entries in each. 232 fullMap = new HashMap<>(1280); 233 // no break. 234 default: 235 if (fullMap == null) { 236 fullMap = new HashMap<>(); 237 } 238 sRevRMap.put(resType, fullMap); 239 } 240 241 for (Field f : inner.getDeclaredFields()) { 242 // only process static final fields. Since the final attribute may have 243 // been altered by layoutlib_create, we only check static 244 if (!isValidRField(f)) { 245 continue; 246 } 247 Class<?> type = f.getType(); 248 if (!type.isArray()) { 249 Integer value = (Integer) f.get(null); 250 //noinspection deprecation 251 sRMap.put(value, Pair.of(resType, f.getName())); 252 fullMap.put(f.getName(), value); 253 } 254 } 255 } 256 } 257 } catch (Exception throwable) { 258 if (log != null) { 259 log.error(LayoutLog.TAG_BROKEN, 260 "Failed to load com.android.internal.R from the layout library jar", 261 throwable, null); 262 } 263 return false; 264 } 265 266 return true; 267 } 268 269 /** 270 * Tests if the field is pubic, static and one of int or int[]. 271 */ isValidRField(Field field)272 private static boolean isValidRField(Field field) { 273 int modifiers = field.getModifiers(); 274 boolean isAcceptable = Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers); 275 Class<?> type = field.getType(); 276 return isAcceptable && type == int.class || 277 (type.isArray() && type.getComponentType() == int.class); 278 279 } 280 parseStyleable()281 private static void parseStyleable() throws Exception { 282 // R.attr doesn't contain all the needed values. There are too many resources in the 283 // framework for all to be in the R class. Only the ones specified manually in 284 // res/values/symbols.xml are put in R class. Since, we need to create a map of all attr 285 // values, we try and find them from the styleables. 286 287 // There were 1500 elements in this map at M timeframe. 288 Map<String, Integer> revRAttrMap = new HashMap<>(2048); 289 sRevRMap.put(ResourceType.ATTR, revRAttrMap); 290 // There were 2000 elements in this map at M timeframe. 291 Map<String, Integer> revRStyleableMap = new HashMap<>(3072); 292 sRevRMap.put(ResourceType.STYLEABLE, revRStyleableMap); 293 Class<?> c = com.android.internal.R.styleable.class; 294 Field[] fields = c.getDeclaredFields(); 295 // Sort the fields to bring all arrays to the beginning, so that indices into the array are 296 // able to refer back to the arrays (i.e. no forward references). 297 Arrays.sort(fields, (o1, o2) -> { 298 if (o1 == o2) { 299 return 0; 300 } 301 Class<?> t1 = o1.getType(); 302 Class<?> t2 = o2.getType(); 303 if (t1.isArray() && !t2.isArray()) { 304 return -1; 305 } else if (t2.isArray() && !t1.isArray()) { 306 return 1; 307 } 308 return o1.getName().compareTo(o2.getName()); 309 }); 310 Map<String, int[]> styleables = new HashMap<>(); 311 for (Field field : fields) { 312 if (!isValidRField(field)) { 313 // Only consider public static fields that are int or int[]. 314 // Don't check the final flag as it may have been modified by layoutlib_create. 315 continue; 316 } 317 String name = field.getName(); 318 if (field.getType().isArray()) { 319 int[] styleableValue = (int[]) field.get(null); 320 styleables.put(name, styleableValue); 321 continue; 322 } 323 // Not an array. 324 String arrayName = name; 325 int[] arrayValue = null; 326 int index; 327 while ((index = arrayName.lastIndexOf('_')) >= 0) { 328 // Find the name of the corresponding styleable. 329 // Search in reverse order so that attrs like LinearLayout_Layout_layout_gravity 330 // are mapped to LinearLayout_Layout and not to LinearLayout. 331 arrayName = arrayName.substring(0, index); 332 arrayValue = styleables.get(arrayName); 333 if (arrayValue != null) { 334 break; 335 } 336 } 337 index = (Integer) field.get(null); 338 if (arrayValue != null) { 339 String attrName = name.substring(arrayName.length() + 1); 340 int attrValue = arrayValue[index]; 341 //noinspection deprecation 342 sRMap.put(attrValue, Pair.of(ResourceType.ATTR, attrName)); 343 revRAttrMap.put(attrName, attrValue); 344 } 345 //noinspection deprecation 346 sRMap.put(index, Pair.of(ResourceType.STYLEABLE, name)); 347 revRStyleableMap.put(name, index); 348 } 349 } 350 351 @Override dispose()352 public boolean dispose() { 353 BridgeAssetManager.clearSystem(); 354 355 // dispose of the default typeface. 356 Typeface_Delegate.resetDefaults(); 357 Typeface.sDynamicTypefaceCache.evictAll(); 358 sProject9PatchCache.clear(); 359 sProjectBitmapCache.clear(); 360 361 return true; 362 } 363 364 /** 365 * Starts a layout session by inflating and rendering it. The method returns a 366 * {@link RenderSession} on which further actions can be taken. 367 * <p/> 368 * If {@link SessionParams} includes the {@link RenderParamsFlags#FLAG_DO_NOT_RENDER_ON_CREATE}, 369 * this method will only inflate the layout but will NOT render it. 370 * @param params the {@link SessionParams} object with all the information necessary to create 371 * the scene. 372 * @return a new {@link RenderSession} object that contains the result of the layout. 373 * @since 5 374 */ 375 @Override createSession(SessionParams params)376 public RenderSession createSession(SessionParams params) { 377 try { 378 Result lastResult; 379 RenderSessionImpl scene = new RenderSessionImpl(params); 380 try { 381 prepareThread(); 382 lastResult = scene.init(params.getTimeout()); 383 if (lastResult.isSuccess()) { 384 lastResult = scene.inflate(); 385 386 boolean doNotRenderOnCreate = Boolean.TRUE.equals( 387 params.getFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE)); 388 if (lastResult.isSuccess() && !doNotRenderOnCreate) { 389 lastResult = scene.render(true /*freshRender*/); 390 } 391 } 392 } finally { 393 scene.release(); 394 cleanupThread(); 395 } 396 397 return new BridgeRenderSession(scene, lastResult); 398 } catch (Throwable t) { 399 // get the real cause of the exception. 400 Throwable t2 = t; 401 while (t2.getCause() != null) { 402 t2 = t2.getCause(); 403 } 404 return new BridgeRenderSession(null, 405 ERROR_UNKNOWN.createResult(t2.getMessage(), t)); 406 } 407 } 408 409 @Override renderDrawable(DrawableParams params)410 public Result renderDrawable(DrawableParams params) { 411 try { 412 Result lastResult; 413 RenderDrawable action = new RenderDrawable(params); 414 try { 415 prepareThread(); 416 lastResult = action.init(params.getTimeout()); 417 if (lastResult.isSuccess()) { 418 lastResult = action.render(); 419 } 420 } finally { 421 action.release(); 422 cleanupThread(); 423 } 424 425 return lastResult; 426 } catch (Throwable t) { 427 // get the real cause of the exception. 428 Throwable t2 = t; 429 while (t2.getCause() != null) { 430 t2 = t.getCause(); 431 } 432 return ERROR_UNKNOWN.createResult(t2.getMessage(), t); 433 } 434 } 435 436 @Override clearCaches(Object projectKey)437 public void clearCaches(Object projectKey) { 438 if (projectKey != null) { 439 sProjectBitmapCache.remove(projectKey); 440 sProject9PatchCache.remove(projectKey); 441 } 442 } 443 444 @Override getViewParent(Object viewObject)445 public Result getViewParent(Object viewObject) { 446 if (viewObject instanceof View) { 447 return Status.SUCCESS.createResult(((View)viewObject).getParent()); 448 } 449 450 throw new IllegalArgumentException("viewObject is not a View"); 451 } 452 453 @Override getViewIndex(Object viewObject)454 public Result getViewIndex(Object viewObject) { 455 if (viewObject instanceof View) { 456 View view = (View) viewObject; 457 ViewParent parentView = view.getParent(); 458 459 if (parentView instanceof ViewGroup) { 460 Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view)); 461 } 462 463 return Status.SUCCESS.createResult(); 464 } 465 466 throw new IllegalArgumentException("viewObject is not a View"); 467 } 468 469 @Override isRtl(String locale)470 public boolean isRtl(String locale) { 471 return isLocaleRtl(locale); 472 } 473 isLocaleRtl(String locale)474 public static boolean isLocaleRtl(String locale) { 475 if (locale == null) { 476 locale = ""; 477 } 478 ULocale uLocale = new ULocale(locale); 479 return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL); 480 } 481 482 /** 483 * Returns the lock for the bridge 484 */ getLock()485 public static ReentrantLock getLock() { 486 return sLock; 487 } 488 489 /** 490 * Prepares the current thread for rendering. 491 * 492 * Note that while this can be called several time, the first call to {@link #cleanupThread()} 493 * will do the clean-up, and make the thread unable to do further scene actions. 494 */ prepareThread()495 public synchronized static void prepareThread() { 496 // we need to make sure the Looper has been initialized for this thread. 497 // this is required for View that creates Handler objects. 498 if (Looper.myLooper() == null) { 499 Looper.prepareMainLooper(); 500 } 501 } 502 503 /** 504 * Cleans up thread-specific data. After this, the thread cannot be used for scene actions. 505 * <p> 506 * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single 507 * call to this will prevent the thread from doing further scene actions 508 */ cleanupThread()509 public synchronized static void cleanupThread() { 510 // clean up the looper 511 Looper_Accessor.cleanupThread(); 512 } 513 getLog()514 public static LayoutLog getLog() { 515 return sCurrentLog; 516 } 517 setLog(LayoutLog log)518 public static void setLog(LayoutLog log) { 519 // check only the thread currently owning the lock can do this. 520 if (!sLock.isHeldByCurrentThread()) { 521 throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); 522 } 523 524 if (log != null) { 525 sCurrentLog = log; 526 } else { 527 sCurrentLog = sDefaultLog; 528 } 529 } 530 531 /** 532 * Returns details of a framework resource from its integer value. 533 * @param value the integer value 534 * @return a Pair containing the resource type and name, or null if the id 535 * does not match any resource. 536 */ 537 @SuppressWarnings("deprecation") resolveResourceId(int value)538 public static Pair<ResourceType, String> resolveResourceId(int value) { 539 Pair<ResourceType, String> pair = sRMap.get(value); 540 if (pair == null) { 541 pair = sDynamicIds.resolveId(value); 542 } 543 return pair; 544 } 545 546 /** 547 * Returns the integer id of a framework resource, from a given resource type and resource name. 548 * <p/> 549 * If no resource is found, it creates a dynamic id for the resource. 550 * 551 * @param type the type of the resource 552 * @param name the name of the resource. 553 * 554 * @return an {@link Integer} containing the resource id. 555 */ 556 @NonNull getResourceId(ResourceType type, String name)557 public static Integer getResourceId(ResourceType type, String name) { 558 Map<String, Integer> map = sRevRMap.get(type); 559 Integer value = null; 560 if (map != null) { 561 value = map.get(name); 562 } 563 564 return value == null ? sDynamicIds.getId(type, name) : value; 565 566 } 567 568 /** 569 * Returns the list of possible enums for a given attribute name. 570 */ getEnumValues(String attributeName)571 public static Map<String, Integer> getEnumValues(String attributeName) { 572 if (sEnumValueMap != null) { 573 return sEnumValueMap.get(attributeName); 574 } 575 576 return null; 577 } 578 579 /** 580 * Returns the platform build properties. 581 */ getPlatformProperties()582 public static Map<String, String> getPlatformProperties() { 583 return sPlatformProperties; 584 } 585 586 /** 587 * Returns the bitmap for a specific path, from a specific project cache, or from the 588 * framework cache. 589 * @param value the path of the bitmap 590 * @param projectKey the key of the project, or null to query the framework cache. 591 * @return the cached Bitmap or null if not found. 592 */ getCachedBitmap(String value, Object projectKey)593 public static Bitmap getCachedBitmap(String value, Object projectKey) { 594 if (projectKey != null) { 595 Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey); 596 if (map != null) { 597 SoftReference<Bitmap> ref = map.get(value); 598 if (ref != null) { 599 return ref.get(); 600 } 601 } 602 } else { 603 SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value); 604 if (ref != null) { 605 return ref.get(); 606 } 607 } 608 609 return null; 610 } 611 612 /** 613 * Sets a bitmap in a project cache or in the framework cache. 614 * @param value the path of the bitmap 615 * @param bmp the Bitmap object 616 * @param projectKey the key of the project, or null to put the bitmap in the framework cache. 617 */ setCachedBitmap(String value, Bitmap bmp, Object projectKey)618 public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) { 619 if (projectKey != null) { 620 Map<String, SoftReference<Bitmap>> map = 621 sProjectBitmapCache.computeIfAbsent(projectKey, k -> new HashMap<>()); 622 623 map.put(value, new SoftReference<>(bmp)); 624 } else { 625 sFrameworkBitmapCache.put(value, new SoftReference<>(bmp)); 626 } 627 } 628 629 /** 630 * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the 631 * framework cache. 632 * @param value the path of the 9 patch 633 * @param projectKey the key of the project, or null to query the framework cache. 634 * @return the cached 9 patch or null if not found. 635 */ getCached9Patch(String value, Object projectKey)636 public static NinePatchChunk getCached9Patch(String value, Object projectKey) { 637 if (projectKey != null) { 638 Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey); 639 640 if (map != null) { 641 SoftReference<NinePatchChunk> ref = map.get(value); 642 if (ref != null) { 643 return ref.get(); 644 } 645 } 646 } else { 647 SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value); 648 if (ref != null) { 649 return ref.get(); 650 } 651 } 652 653 return null; 654 } 655 656 /** 657 * Sets a 9 patch chunk in a project cache or in the framework cache. 658 * @param value the path of the 9 patch 659 * @param ninePatch the 9 patch object 660 * @param projectKey the key of the project, or null to put the bitmap in the framework cache. 661 */ setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey)662 public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) { 663 if (projectKey != null) { 664 Map<String, SoftReference<NinePatchChunk>> map = 665 sProject9PatchCache.computeIfAbsent(projectKey, k -> new HashMap<>()); 666 667 map.put(value, new SoftReference<>(ninePatch)); 668 } else { 669 sFramework9PatchCache.put(value, new SoftReference<>(ninePatch)); 670 } 671 } 672 } 673