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