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