• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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