1 /* 2 * Copyright (C) 2010 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.impl; 18 19 import com.android.ide.common.rendering.api.HardwareConfig; 20 import com.android.ide.common.rendering.api.ILayoutLog; 21 import com.android.ide.common.rendering.api.RenderParams; 22 import com.android.ide.common.rendering.api.RenderResources; 23 import com.android.ide.common.rendering.api.Result; 24 import com.android.layoutlib.bridge.Bridge; 25 import com.android.layoutlib.bridge.android.BridgeContext; 26 import com.android.layoutlib.bridge.android.RenderParamsFlags; 27 import com.android.resources.Density; 28 import com.android.resources.ScreenOrientation; 29 import com.android.resources.ScreenRound; 30 import com.android.resources.ScreenSize; 31 import com.android.tools.layoutlib.annotations.NotNull; 32 import com.android.tools.layoutlib.annotations.Nullable; 33 import com.android.tools.layoutlib.annotations.VisibleForTesting; 34 35 import android.animation.AnimationHandler; 36 import android.animation.PropertyValuesHolder_Accessor; 37 import android.content.Context; 38 import android.content.res.Configuration; 39 import android.graphics.Bitmap; 40 import android.graphics.Rect; 41 import android.graphics.drawable.AdaptiveIconDrawable_Delegate; 42 import android.os.HandlerThread_Delegate; 43 import android.os.SystemProperties; 44 import android.util.DisplayMetrics; 45 import android.view.IWindowManager; 46 import android.view.IWindowManagerImpl; 47 import android.view.ViewConfiguration_Accessor; 48 import android.view.WindowManagerGlobal_Delegate; 49 import android.view.WindowManagerImpl; 50 import android.view.accessibility.AccessibilityInteractionClient_Accessor; 51 import android.view.inputmethod.InputMethodManager_Accessor; 52 53 import java.util.Collections; 54 import java.util.Locale; 55 import java.util.Set; 56 import java.util.WeakHashMap; 57 import java.util.concurrent.TimeUnit; 58 import java.util.concurrent.locks.ReentrantLock; 59 60 import static android.os._Original_Build.VERSION.SDK_INT; 61 import static android.view.Surface.ROTATION_0; 62 import static android.view.Surface.ROTATION_90; 63 import static com.android.ide.common.rendering.api.Result.Status.ERROR_LOCK_INTERRUPTED; 64 import static com.android.ide.common.rendering.api.Result.Status.ERROR_TIMEOUT; 65 import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; 66 import static com.android.layoutlib.bridge.android.RenderParamsFlags.FLAG_KEY_CACHE_BITMAPS; 67 import static com.android.layoutlib.bridge.android.RenderParamsFlags.FLAG_KEY_SHOW_CUTOUT; 68 69 /** 70 * Base class for rendering action. 71 * 72 * It provides life-cycle methods to init and stop the rendering. 73 * The most important methods are: 74 * {@link #init(long)} and {@link #acquire(long)} to start a rendering and {@link #release()} 75 * after the rendering. 76 * 77 * 78 * @param <T> the {@link RenderParams} implementation 79 * 80 */ 81 public abstract class RenderAction<T extends RenderParams> { 82 /** 83 * Static field to store an SDK version coming from the render configuration. 84 * This is to be accessed when wanting to know the simulated SDK version instead 85 * of Build.VERSION.SDK_INT. 86 */ 87 @SuppressWarnings("WeakerAccess") // Field accessed from Studio 88 public static int sSimulatedSdk; 89 90 private static final Set<String> COMPOSE_CLASS_FQNS = 91 Set.of("androidx.compose.ui.tooling.ComposeViewAdapter", 92 "androidx.compose.ui.tooling.preview.ComposeViewAdapter"); 93 94 /** 95 * The current context being rendered. This is set through {@link #acquire(long)} and 96 * {@link #init(long)}, and unset in {@link #release()}. 97 */ 98 @VisibleForTesting 99 static BridgeContext sCurrentContext = null; 100 101 private final T mParams; 102 103 private BridgeContext mContext; 104 105 private static final Object sContextLock = new Object(); 106 private static final Set<BridgeContext> sContexts = 107 Collections.newSetFromMap(new WeakHashMap<>()); 108 109 /** 110 * Creates a renderAction. 111 * <p> 112 * This <b>must</b> be followed by a call to {@link RenderAction#init(long)}, which act as a 113 * call to {@link RenderAction#acquire(long)} 114 * 115 * @param params the RenderParams. This must be a copy that the action can keep 116 * 117 */ RenderAction(T params)118 protected RenderAction(T params) { 119 mParams = params; 120 sSimulatedSdk = SDK_INT; 121 } 122 123 /** 124 * Initializes and acquires the scene, creating various Android objects such as context, 125 * inflater, and parser. 126 * 127 * @param timeout the time to wait if another rendering is happening. 128 * 129 * @return whether the scene was prepared 130 * 131 * @see #acquire(long) 132 * @see #release() 133 */ init(long timeout)134 public Result init(long timeout) { 135 // acquire the lock. if the result is null, lock was just acquired, otherwise, return 136 // the result. 137 Result result = acquireLock(timeout); 138 if (result != null) { 139 return result; 140 } 141 142 HardwareConfig hardwareConfig = mParams.getHardwareConfig(); 143 144 // setup the display Metrics. 145 SystemProperties.set("qemu.sf.lcd_density", 146 Integer.toString(hardwareConfig.getDensity().getDpiValue())); 147 DisplayMetrics metrics = new DisplayMetrics(); 148 metrics.densityDpi = metrics.noncompatDensityDpi = 149 hardwareConfig.getDensity().getDpiValue(); 150 151 metrics.density = metrics.noncompatDensity = 152 metrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT; 153 154 metrics.scaledDensity = metrics.noncompatScaledDensity = metrics.density; 155 156 if (hardwareConfig.getOrientation() == ScreenOrientation.PORTRAIT) { 157 metrics.widthPixels = metrics.noncompatWidthPixels = hardwareConfig.getScreenWidth(); 158 metrics.heightPixels = metrics.noncompatHeightPixels = hardwareConfig.getScreenHeight(); 159 } else { 160 metrics.widthPixels = metrics.noncompatWidthPixels = hardwareConfig.getScreenHeight(); 161 metrics.heightPixels = metrics.noncompatHeightPixels = hardwareConfig.getScreenWidth(); 162 } 163 metrics.xdpi = metrics.noncompatXdpi = hardwareConfig.getXdpi(); 164 metrics.ydpi = metrics.noncompatYdpi = hardwareConfig.getYdpi(); 165 166 RenderResources resources = mParams.getResources(); 167 168 // sets the custom adaptive icon path 169 AdaptiveIconDrawable_Delegate.sPath = 170 mParams.getFlag(RenderParamsFlags.FLAG_KEY_ADAPTIVE_ICON_MASK_PATH); 171 172 // build the context 173 mContext = new BridgeContext(mParams.getProjectKey(), metrics, resources, 174 mParams.getAssets(), mParams.getLayoutlibCallback(), getConfiguration(mParams), 175 mParams.getTargetSdkVersion(), mParams.isRtlSupported()); 176 177 synchronized (sContextLock) { 178 sContexts.add(mContext); 179 } 180 setUp(); 181 182 return SUCCESS.createResult(); 183 } 184 updateHardwareConfiguration(HardwareConfig hardwareConfig)185 public void updateHardwareConfiguration(HardwareConfig hardwareConfig) { 186 mParams.setHardwareConfig(hardwareConfig); 187 } 188 189 /** 190 * Prepares the scene for action. 191 * <p> 192 * This call is blocking if another rendering/inflating is currently happening, and will return 193 * whether the preparation worked. 194 * 195 * The preparation can fail if another rendering took too long and the timeout was elapsed. 196 * 197 * More than one call to this from the same thread will have no effect and will return 198 * {@link Result.Status#SUCCESS}. 199 * 200 * After scene actions have taken place, only one call to {@link #release()} must be 201 * done. 202 * 203 * @param timeout the time to wait if another rendering is happening. 204 * 205 * @return whether the scene was prepared 206 * 207 * @see #release() 208 * 209 * @throws IllegalStateException if {@link #init(long)} was never called. 210 */ acquire(long timeout)211 public Result acquire(long timeout) { 212 if (mContext == null) { 213 throw new IllegalStateException("After scene creation, #init() must be called"); 214 } 215 216 // acquire the lock. if the result is null, lock was just acquired, otherwise, return 217 // the result. 218 Result result = acquireLock(timeout); 219 if (result != null) { 220 return result; 221 } 222 223 setUp(); 224 225 return SUCCESS.createResult(); 226 } 227 228 /** 229 * Acquire the lock so that the scene can be acted upon. 230 * <p> 231 * This returns null if the lock was just acquired, otherwise it returns 232 * {@link Result.Status#SUCCESS} if the lock already belonged to that thread, or another 233 * instance (see {@link Result#getStatus()}) if an error occurred. 234 * 235 * @param timeout the time to wait if another rendering is happening. 236 * @return null if the lock was just acquire or another result depending on the state. 237 * 238 * @throws IllegalStateException if the current context is different than the one owned by 239 * the scene. 240 */ acquireLock(long timeout)241 private Result acquireLock(long timeout) { 242 ReentrantLock lock = Bridge.getLock(); 243 if (!lock.isHeldByCurrentThread()) { 244 try { 245 boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); 246 247 if (!acquired) { 248 return ERROR_TIMEOUT.createResult(); 249 } 250 } catch (InterruptedException e) { 251 return ERROR_LOCK_INTERRUPTED.createResult(); 252 } 253 } else { 254 // This thread holds the lock already. Checks that this wasn't for a different context. 255 // If this is called by init, mContext will be null and so should sCurrentContext 256 // anyway 257 if (mContext != sCurrentContext) { 258 throw new IllegalStateException("Acquiring different scenes from same thread without releases"); 259 } 260 return SUCCESS.createResult(); 261 } 262 263 return null; 264 } 265 266 /** 267 * Cleans up the scene after an action. 268 */ release()269 public void release() { 270 ReentrantLock lock = Bridge.getLock(); 271 272 // with the use of finally blocks, it is possible to find ourself calling this 273 // without a successful call to prepareScene. This test makes sure that unlock() will 274 // not throw IllegalMonitorStateException. 275 if (lock.isHeldByCurrentThread()) { 276 tearDown(); 277 lock.unlock(); 278 } 279 } 280 281 /** 282 * Sets up the session for rendering. 283 * <p/> 284 * The counterpart is {@link #tearDown()}. 285 */ setUp()286 private void setUp() { 287 // setup the ParserFactory 288 ParserFactory.setParserFactory(mParams.getLayoutlibCallback()); 289 290 // make sure the Resources object references the context (and other objects) for this 291 // scene 292 mContext.initResources(mParams.getAssets()); 293 sCurrentContext = mContext; 294 mContext.applyWallpaper(mParams.getFlag(RenderParamsFlags.FLAG_KEY_WALLPAPER_PATH)); 295 mContext.setUseThemedIcon( 296 Boolean.TRUE.equals(mParams.getFlag(RenderParamsFlags.FLAG_KEY_USE_THEMED_ICON))); 297 mContext.setForceMonochromeIcon(Boolean.TRUE.equals( 298 mParams.getFlag(RenderParamsFlags.FLAG_KEY_FORCE_MONOCHROME_ICON))); 299 300 // Set-up WindowManager 301 // FIXME: find those out, and possibly add them to the render params 302 boolean hasNavigationBar = true; 303 IWindowManager iwm = new IWindowManagerImpl(getContext().getConfiguration(), 304 getContext().getMetrics(), ROTATION_0, hasNavigationBar); 305 WindowManagerGlobal_Delegate.setWindowManagerService(iwm); 306 if (Boolean.TRUE.equals(mParams.getFlag(FLAG_KEY_SHOW_CUTOUT))) { 307 ((WindowManagerImpl) mContext.getSystemService(Context.WINDOW_SERVICE)) 308 .setupDisplayCutout(); 309 } 310 311 ILayoutLog currentLog = mParams.getLog(); 312 Bridge.setLog(currentLog); 313 mContext.getRenderResources().setLogger(currentLog); 314 AnimationHandler.sAnimatorHandler = mContext.getAnimationHandlerThreadLocal(); 315 } 316 317 /** 318 * Tear down the session after rendering. 319 * <p/> 320 * The counterpart is {@link #setUp()}. 321 */ tearDown()322 private void tearDown() { 323 // The context may be null, if there was an error during init(). 324 if (mContext != null) { 325 // Make sure to remove static references, otherwise we could not unload the lib 326 mContext.disposeResources(); 327 } 328 329 // clear the stored ViewConfiguration since the map is per density and not per context. 330 ViewConfiguration_Accessor.clearConfigurations(); 331 332 // remove the InputMethodManager 333 InputMethodManager_Accessor.tearDownEditMode(); 334 335 Bridge.setLog(null); 336 if (mContext != null) { 337 mContext.getRenderResources().setLogger(null); 338 } 339 ParserFactory.setParserFactory(null); 340 341 PropertyValuesHolder_Accessor.clearClassCaches(); 342 AccessibilityInteractionClient_Accessor.clearCaches(); 343 if (!Boolean.TRUE.equals(mParams.getFlag(FLAG_KEY_CACHE_BITMAPS))) { 344 // Clear caches except if the flag is explicitly set to true. 345 Bitmap.sAllBitmaps.clear(); 346 Bridge.clearBitmapCaches(mParams.getProjectKey()); 347 } 348 } 349 getCurrentContext()350 public static BridgeContext getCurrentContext() { 351 return sCurrentContext; 352 } 353 getParams()354 protected T getParams() { 355 return mParams; 356 } 357 getContext()358 protected BridgeContext getContext() { 359 return mContext; 360 } 361 362 /** 363 * Returns the log associated with the session. 364 * @return the log or null if there are none. 365 */ getLog()366 public ILayoutLog getLog() { 367 if (mParams != null) { 368 return mParams.getLog(); 369 } 370 371 return null; 372 } 373 374 /** 375 * Checks that the lock is owned by the current thread and that the current context is the one 376 * from this scene. 377 * 378 * @throws IllegalStateException if the current context is different than the one owned by 379 * the scene, or if {@link #acquire(long)} was not called. 380 */ checkLock()381 protected void checkLock() { 382 ReentrantLock lock = Bridge.getLock(); 383 if (!lock.isHeldByCurrentThread()) { 384 throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); 385 } 386 if (sCurrentContext != mContext) { 387 throw new IllegalStateException("Thread acquired a scene but is rendering a different one"); 388 } 389 } 390 391 // VisibleForTesting getConfiguration(RenderParams params)392 public static Configuration getConfiguration(RenderParams params) { 393 Configuration config = new Configuration(); 394 395 HardwareConfig hardwareConfig = params.getHardwareConfig(); 396 397 ScreenSize screenSize = hardwareConfig.getScreenSize(); 398 if (screenSize != null) { 399 switch (screenSize) { 400 case SMALL: 401 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_SMALL; 402 break; 403 case NORMAL: 404 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_NORMAL; 405 break; 406 case LARGE: 407 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_LARGE; 408 break; 409 case XLARGE: 410 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_XLARGE; 411 break; 412 } 413 } 414 415 Density density = hardwareConfig.getDensity(); 416 if (density == null) { 417 density = Density.MEDIUM; 418 } 419 420 config.screenWidthDp = hardwareConfig.getScreenWidth() * 160 / density.getDpiValue(); 421 config.screenHeightDp = hardwareConfig.getScreenHeight() * 160 / density.getDpiValue(); 422 config.smallestScreenWidthDp = Math.min(config.screenHeightDp, config.screenWidthDp); 423 config.densityDpi = density.getDpiValue(); 424 425 // never run in compat mode: 426 config.compatScreenWidthDp = config.screenWidthDp; 427 config.compatScreenHeightDp = config.screenHeightDp; 428 429 ScreenOrientation orientation = hardwareConfig.getOrientation(); 430 if (orientation != null) { 431 switch (orientation) { 432 case PORTRAIT: 433 config.orientation = Configuration.ORIENTATION_PORTRAIT; 434 config.windowConfiguration.setDisplayRotation(ROTATION_0); 435 break; 436 case LANDSCAPE: 437 config.orientation = Configuration.ORIENTATION_LANDSCAPE; 438 config.windowConfiguration.setDisplayRotation(ROTATION_90); 439 break; 440 case SQUARE: 441 //noinspection deprecation 442 config.orientation = Configuration.ORIENTATION_SQUARE; 443 config.windowConfiguration.setDisplayRotation(ROTATION_0); 444 break; 445 } 446 } else { 447 config.orientation = Configuration.ORIENTATION_UNDEFINED; 448 } 449 450 ScreenRound roundness = hardwareConfig.getScreenRoundness(); 451 if (roundness != null) { 452 switch (roundness) { 453 case ROUND: 454 config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES; 455 break; 456 case NOTROUND: 457 config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_NO; 458 } 459 } else { 460 config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_UNDEFINED; 461 } 462 String locale = params.getLocale(); 463 if (locale != null && !locale.isEmpty()) config.locale = new Locale(locale); 464 465 config.fontScale = params.getFontScale(); 466 config.uiMode = params.getUiMode(); 467 468 Rect bounds = new Rect(0, 0, hardwareConfig.getScreenWidth(), 469 hardwareConfig.getScreenHeight()); 470 config.windowConfiguration.setBounds(bounds); 471 config.windowConfiguration.setAppBounds(bounds); 472 config.windowConfiguration.setMaxBounds(bounds); 473 // TODO: fill in more config info. 474 475 return config; 476 } 477 478 @Nullable findComposeClassLoader(@otNull BridgeContext context)479 private static ClassLoader findComposeClassLoader(@NotNull BridgeContext context) { 480 for (String composeClassName: COMPOSE_CLASS_FQNS) { 481 try { 482 return context.getLayoutlibCallback().findClass(composeClassName).getClassLoader(); 483 } catch (Throwable ignore) {} 484 } 485 486 return null; 487 } 488 489 @Nullable findContextFor(@otNull ClassLoader classLoader)490 public static BridgeContext findContextFor(@NotNull ClassLoader classLoader) { 491 synchronized (sContextLock) { 492 for (BridgeContext c : RenderAction.sContexts) { 493 if (c == null) { 494 continue; 495 } 496 try { 497 if (findComposeClassLoader(c) == classLoader) { 498 return c; 499 } 500 } catch (Throwable ignore) { 501 } 502 } 503 return null; 504 } 505 } 506 dispose()507 protected void dispose() { 508 synchronized (sContextLock) { 509 sContexts.remove(mContext); 510 } 511 512 if (sCurrentContext != null) { 513 // quit HandlerThread created during this session. 514 HandlerThread_Delegate.cleanUp(sCurrentContext); 515 516 AnimationHandler animationHandler = 517 sCurrentContext.getAnimationHandlerThreadLocal().get(); 518 if (animationHandler != null) { 519 animationHandler.mDelayedCallbackStartTime.clear(); 520 animationHandler.mAnimationCallbacks.clear(); 521 animationHandler.mCommitCallbacks.clear(); 522 } 523 // Clear the ThreadLocal to avoid memory leaks 524 sCurrentContext.getAnimationHandlerThreadLocal().remove(); 525 } 526 527 sCurrentContext = null; 528 } 529 } 530