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