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