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