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