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
17package com.android.layoutlib.bridge.impl;
18
19import com.android.ide.common.rendering.api.HardwareConfig;
20import com.android.ide.common.rendering.api.LayoutLog;
21import com.android.ide.common.rendering.api.RenderParams;
22import com.android.ide.common.rendering.api.RenderResources;
23import com.android.ide.common.rendering.api.RenderResources.FrameworkResourceIdProvider;
24import com.android.ide.common.rendering.api.Result;
25import com.android.layoutlib.bridge.Bridge;
26import com.android.layoutlib.bridge.android.BridgeContext;
27import com.android.resources.Density;
28import com.android.resources.ResourceType;
29import com.android.resources.ScreenOrientation;
30import com.android.resources.ScreenRound;
31import com.android.resources.ScreenSize;
32import com.android.tools.layoutlib.annotations.VisibleForTesting;
33
34import android.content.res.Configuration;
35import android.os.HandlerThread_Delegate;
36import android.util.DisplayMetrics;
37import android.view.IWindowManager;
38import android.view.IWindowManagerImpl;
39import android.view.Surface;
40import android.view.ViewConfiguration_Accessor;
41import android.view.WindowManagerGlobal_Delegate;
42import android.view.inputmethod.InputMethodManager;
43import android.view.inputmethod.InputMethodManager_Accessor;
44
45import java.util.Locale;
46import java.util.concurrent.TimeUnit;
47import java.util.concurrent.locks.ReentrantLock;
48
49import static com.android.ide.common.rendering.api.Result.Status.ERROR_LOCK_INTERRUPTED;
50import static com.android.ide.common.rendering.api.Result.Status.ERROR_TIMEOUT;
51import 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 */
65public abstract class RenderAction<T extends RenderParams> extends FrameworkResourceIdProvider {
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     */
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     */
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
134        setUp();
135
136        return SUCCESS.createResult();
137    }
138
139    /**
140     * Prepares the scene for action.
141     * <p>
142     * This call is blocking if another rendering/inflating is currently happening, and will return
143     * whether the preparation worked.
144     *
145     * The preparation can fail if another rendering took too long and the timeout was elapsed.
146     *
147     * More than one call to this from the same thread will have no effect and will return
148     * {@link Result.Status#SUCCESS}.
149     *
150     * After scene actions have taken place, only one call to {@link #release()} must be
151     * done.
152     *
153     * @param timeout the time to wait if another rendering is happening.
154     *
155     * @return whether the scene was prepared
156     *
157     * @see #release()
158     *
159     * @throws IllegalStateException if {@link #init(long)} was never called.
160     */
161    public Result acquire(long timeout) {
162        if (mContext == null) {
163            throw new IllegalStateException("After scene creation, #init() must be called");
164        }
165
166        // acquire the lock. if the result is null, lock was just acquired, otherwise, return
167        // the result.
168        Result result = acquireLock(timeout);
169        if (result != null) {
170            return result;
171        }
172
173        setUp();
174
175        return SUCCESS.createResult();
176    }
177
178    /**
179     * Acquire the lock so that the scene can be acted upon.
180     * <p>
181     * This returns null if the lock was just acquired, otherwise it returns
182     * {@link Result.Status#SUCCESS} if the lock already belonged to that thread, or another
183     * instance (see {@link Result#getStatus()}) if an error occurred.
184     *
185     * @param timeout the time to wait if another rendering is happening.
186     * @return null if the lock was just acquire or another result depending on the state.
187     *
188     * @throws IllegalStateException if the current context is different than the one owned by
189     *      the scene.
190     */
191    private Result acquireLock(long timeout) {
192        ReentrantLock lock = Bridge.getLock();
193        if (!lock.isHeldByCurrentThread()) {
194            try {
195                boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
196
197                if (!acquired) {
198                    return ERROR_TIMEOUT.createResult();
199                }
200            } catch (InterruptedException e) {
201                return ERROR_LOCK_INTERRUPTED.createResult();
202            }
203        } else {
204            // This thread holds the lock already. Checks that this wasn't for a different context.
205            // If this is called by init, mContext will be null and so should sCurrentContext
206            // anyway
207            if (mContext != sCurrentContext) {
208                throw new IllegalStateException("Acquiring different scenes from same thread without releases");
209            }
210            return SUCCESS.createResult();
211        }
212
213        return null;
214    }
215
216    /**
217     * Cleans up the scene after an action.
218     */
219    public void release() {
220        ReentrantLock lock = Bridge.getLock();
221
222        // with the use of finally blocks, it is possible to find ourself calling this
223        // without a successful call to prepareScene. This test makes sure that unlock() will
224        // not throw IllegalMonitorStateException.
225        if (lock.isHeldByCurrentThread()) {
226            tearDown();
227            lock.unlock();
228        }
229    }
230
231    /**
232     * Sets up the session for rendering.
233     * <p/>
234     * The counterpart is {@link #tearDown()}.
235     */
236    private void setUp() {
237        // setup the ParserFactory
238        ParserFactory.setParserFactory(mParams.getLayoutlibCallback().getParserFactory());
239
240        // make sure the Resources object references the context (and other objects) for this
241        // scene
242        mContext.initResources();
243        sCurrentContext = mContext;
244
245        // create an InputMethodManager
246        InputMethodManager.getInstance();
247
248        // Set-up WindowManager
249        // FIXME: find those out, and possibly add them to the render params
250        boolean hasNavigationBar = true;
251        //noinspection ConstantConditions
252        IWindowManager iwm = new IWindowManagerImpl(getContext().getConfiguration(),
253                getContext().getMetrics(), Surface.ROTATION_0, hasNavigationBar);
254        WindowManagerGlobal_Delegate.setWindowManagerService(iwm);
255
256        LayoutLog currentLog = mParams.getLog();
257        Bridge.setLog(currentLog);
258        mContext.getRenderResources().setFrameworkResourceIdProvider(this);
259        mContext.getRenderResources().setLogger(currentLog);
260    }
261
262    /**
263     * Tear down the session after rendering.
264     * <p/>
265     * The counterpart is {@link #setUp()}.
266     */
267    private void tearDown() {
268        // The context may be null, if there was an error during init().
269        if (mContext != null) {
270            // Make sure to remove static references, otherwise we could not unload the lib
271            mContext.disposeResources();
272        }
273
274        if (sCurrentContext != null) {
275            // quit HandlerThread created during this session.
276            HandlerThread_Delegate.cleanUp(sCurrentContext);
277        }
278
279        // clear the stored ViewConfiguration since the map is per density and not per context.
280        ViewConfiguration_Accessor.clearConfigurations();
281
282        // remove the InputMethodManager
283        InputMethodManager_Accessor.resetInstance();
284
285        sCurrentContext = null;
286
287        Bridge.setLog(null);
288        if (mContext != null) {
289            mContext.getRenderResources().setFrameworkResourceIdProvider(null);
290            mContext.getRenderResources().setLogger(null);
291        }
292        ParserFactory.setParserFactory(null);
293    }
294
295    public static BridgeContext getCurrentContext() {
296        return sCurrentContext;
297    }
298
299    protected T getParams() {
300        return mParams;
301    }
302
303    protected BridgeContext getContext() {
304        return mContext;
305    }
306
307    /**
308     * Returns the log associated with the session.
309     * @return the log or null if there are none.
310     */
311    public LayoutLog getLog() {
312        if (mParams != null) {
313            return mParams.getLog();
314        }
315
316        return null;
317    }
318
319    /**
320     * Checks that the lock is owned by the current thread and that the current context is the one
321     * from this scene.
322     *
323     * @throws IllegalStateException if the current context is different than the one owned by
324     *      the scene, or if {@link #acquire(long)} was not called.
325     */
326    protected void checkLock() {
327        ReentrantLock lock = Bridge.getLock();
328        if (!lock.isHeldByCurrentThread()) {
329            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
330        }
331        if (sCurrentContext != mContext) {
332            throw new IllegalStateException("Thread acquired a scene but is rendering a different one");
333        }
334    }
335
336    // VisibleForTesting
337    public static Configuration getConfiguration(RenderParams params) {
338        Configuration config = new Configuration();
339
340        HardwareConfig hardwareConfig = params.getHardwareConfig();
341
342        ScreenSize screenSize = hardwareConfig.getScreenSize();
343        if (screenSize != null) {
344            switch (screenSize) {
345                case SMALL:
346                    config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_SMALL;
347                    break;
348                case NORMAL:
349                    config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_NORMAL;
350                    break;
351                case LARGE:
352                    config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_LARGE;
353                    break;
354                case XLARGE:
355                    config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_XLARGE;
356                    break;
357            }
358        }
359
360        Density density = hardwareConfig.getDensity();
361        if (density == null) {
362            density = Density.MEDIUM;
363        }
364
365        config.screenWidthDp = hardwareConfig.getScreenWidth() / density.getDpiValue();
366        config.screenHeightDp = hardwareConfig.getScreenHeight() / density.getDpiValue();
367        if (config.screenHeightDp < config.screenWidthDp) {
368            //noinspection SuspiciousNameCombination
369            config.smallestScreenWidthDp = config.screenHeightDp;
370        } else {
371            config.smallestScreenWidthDp = config.screenWidthDp;
372        }
373        config.densityDpi = density.getDpiValue();
374
375        // never run in compat mode:
376        config.compatScreenWidthDp = config.screenWidthDp;
377        config.compatScreenHeightDp = config.screenHeightDp;
378
379        ScreenOrientation orientation = hardwareConfig.getOrientation();
380        if (orientation != null) {
381            switch (orientation) {
382            case PORTRAIT:
383                config.orientation = Configuration.ORIENTATION_PORTRAIT;
384                break;
385            case LANDSCAPE:
386                config.orientation = Configuration.ORIENTATION_LANDSCAPE;
387                break;
388            case SQUARE:
389                //noinspection deprecation
390                config.orientation = Configuration.ORIENTATION_SQUARE;
391                break;
392            }
393        } else {
394            config.orientation = Configuration.ORIENTATION_UNDEFINED;
395        }
396
397        ScreenRound roundness = hardwareConfig.getScreenRoundness();
398        if (roundness != null) {
399            switch (roundness) {
400                case ROUND:
401                    config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES;
402                    break;
403                case NOTROUND:
404                    config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_NO;
405            }
406        } else {
407            config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_UNDEFINED;
408        }
409        String locale = params.getLocale();
410        if (locale != null && !locale.isEmpty()) config.locale = new Locale(locale);
411
412        // TODO: fill in more config info.
413
414        return config;
415    }
416
417
418    // --- FrameworkResourceIdProvider methods
419
420    @Override
421    public Integer getId(ResourceType resType, String resName) {
422        return Bridge.getResourceId(resType, resName);
423    }
424}
425