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