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