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