• 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.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