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