Bridge.java revision 4d7f301f94b9d2dda0ef109e9991ad2d77442f75
1/*
2 * Copyright (C) 2008 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;
18
19import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
20import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
21
22import com.android.ide.common.rendering.api.Capability;
23import com.android.ide.common.rendering.api.DrawableParams;
24import com.android.ide.common.rendering.api.LayoutLog;
25import com.android.ide.common.rendering.api.RenderSession;
26import com.android.ide.common.rendering.api.Result;
27import com.android.ide.common.rendering.api.Result.Status;
28import com.android.ide.common.rendering.api.SessionParams;
29import com.android.layoutlib.bridge.impl.FontLoader;
30import com.android.layoutlib.bridge.impl.RenderDrawable;
31import com.android.layoutlib.bridge.impl.RenderSessionImpl;
32import com.android.layoutlib.bridge.util.DynamicIdMap;
33import com.android.ninepatch.NinePatchChunk;
34import com.android.resources.ResourceType;
35import com.android.tools.layoutlib.create.MethodAdapter;
36import com.android.tools.layoutlib.create.OverrideMethod;
37import com.android.util.Pair;
38
39import android.content.res.BridgeAssetManager;
40import android.graphics.Bitmap;
41import android.graphics.Typeface_Accessor;
42import android.graphics.Typeface_Delegate;
43import android.os.Looper;
44import android.os.Looper_Accessor;
45import android.view.View;
46import android.view.ViewGroup;
47import android.view.ViewParent;
48
49import java.io.File;
50import java.lang.ref.SoftReference;
51import java.lang.reflect.Field;
52import java.lang.reflect.Modifier;
53import java.util.Arrays;
54import java.util.EnumMap;
55import java.util.EnumSet;
56import java.util.HashMap;
57import java.util.Map;
58import java.util.concurrent.locks.ReentrantLock;
59
60/**
61 * Main entry point of the LayoutLib Bridge.
62 * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call
63 * {@link #createScene(SceneParams)}
64 */
65public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
66
67    public static class StaticMethodNotImplementedException extends RuntimeException {
68        private static final long serialVersionUID = 1L;
69
70        public StaticMethodNotImplementedException(String msg) {
71            super(msg);
72        }
73    }
74
75    /**
76     * Lock to ensure only one rendering/inflating happens at a time.
77     * This is due to some singleton in the Android framework.
78     */
79    private final static ReentrantLock sLock = new ReentrantLock();
80
81    /**
82     * Maps from id to resource type/name. This is for com.android.internal.R
83     */
84    private final static Map<Integer, Pair<ResourceType, String>> sRMap =
85        new HashMap<Integer, Pair<ResourceType, String>>();
86
87    /**
88     * Same as sRMap except for int[] instead of int resources. This is for android.R only.
89     */
90    private final static Map<IntArray, String> sRArrayMap = new HashMap<IntArray, String>();
91    /**
92     * Reverse map compared to sRMap, resource type -> (resource name -> id).
93     * This is for com.android.internal.R.
94     */
95    private final static Map<ResourceType, Map<String, Integer>> sRevRMap =
96        new EnumMap<ResourceType, Map<String,Integer>>(ResourceType.class);
97
98    // framework resources are defined as 0x01XX#### where XX is the resource type (layout,
99    // drawable, etc...). Using FF as the type allows for 255 resource types before we get a
100    // collision which should be fine.
101    private final static int DYNAMIC_ID_SEED_START = 0x01ff0000;
102    private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START);
103
104    private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache =
105        new HashMap<Object, Map<String, SoftReference<Bitmap>>>();
106    private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache =
107        new HashMap<Object, Map<String, SoftReference<NinePatchChunk>>>();
108
109    private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache =
110        new HashMap<String, SoftReference<Bitmap>>();
111    private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache =
112        new HashMap<String, SoftReference<NinePatchChunk>>();
113
114    private static Map<String, Map<String, Integer>> sEnumValueMap;
115    private static Map<String, String> sPlatformProperties;
116
117    /**
118     * int[] wrapper to use as keys in maps.
119     */
120    private final static class IntArray {
121        private int[] mArray;
122
123        private IntArray() {
124            // do nothing
125        }
126
127        private IntArray(int[] a) {
128            mArray = a;
129        }
130
131        private void set(int[] a) {
132            mArray = a;
133        }
134
135        @Override
136        public int hashCode() {
137            return Arrays.hashCode(mArray);
138        }
139
140        @Override
141        public boolean equals(Object obj) {
142            if (this == obj) return true;
143            if (obj == null) return false;
144            if (getClass() != obj.getClass()) return false;
145
146            IntArray other = (IntArray) obj;
147            if (!Arrays.equals(mArray, other.mArray)) return false;
148            return true;
149        }
150    }
151
152    /** Instance of IntArrayWrapper to be reused in {@link #resolveResourceId(int[])}. */
153    private final static IntArray sIntArrayWrapper = new IntArray();
154
155    /**
156     * A default log than prints to stdout/stderr.
157     */
158    private final static LayoutLog sDefaultLog = new LayoutLog() {
159        @Override
160        public void error(String tag, String message, Object data) {
161            System.err.println(message);
162        }
163
164        @Override
165        public void error(String tag, String message, Throwable throwable, Object data) {
166            System.err.println(message);
167        }
168
169        @Override
170        public void warning(String tag, String message, Object data) {
171            System.out.println(message);
172        }
173    };
174
175    /**
176     * Current log.
177     */
178    private static LayoutLog sCurrentLog = sDefaultLog;
179
180    private EnumSet<Capability> mCapabilities;
181
182    @Override
183    public int getApiLevel() {
184        return com.android.ide.common.rendering.api.Bridge.API_CURRENT;
185    }
186
187    @Override
188    public EnumSet<Capability> getCapabilities() {
189        return mCapabilities;
190    }
191
192    @Override
193    public boolean init(Map<String,String> platformProperties,
194            File fontLocation,
195            Map<String, Map<String, Integer>> enumValueMap,
196            LayoutLog log) {
197        sPlatformProperties = platformProperties;
198        sEnumValueMap = enumValueMap;
199
200        // don't use EnumSet.allOf(), because the bridge doesn't come with its specific version
201        // of layoutlib_api. It is provided by the client which could have a more recent version
202        // with newer, unsupported capabilities.
203        mCapabilities = EnumSet.of(
204                Capability.UNBOUND_RENDERING,
205                Capability.CUSTOM_BACKGROUND_COLOR,
206                Capability.RENDER,
207                Capability.LAYOUT_ONLY,
208                Capability.EMBEDDED_LAYOUT,
209                Capability.VIEW_MANIPULATION,
210                Capability.PLAY_ANIMATION,
211                Capability.ANIMATED_VIEW_MANIPULATION,
212                Capability.ADAPTER_BINDING,
213                Capability.EXTENDED_VIEWINFO);
214
215
216        BridgeAssetManager.initSystem();
217
218        // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener
219        // on static (native) methods which prints the signature on the console and
220        // throws an exception.
221        // This is useful when testing the rendering in ADT to identify static native
222        // methods that are ignored -- layoutlib_create makes them returns 0/false/null
223        // which is generally OK yet might be a problem, so this is how you'd find out.
224        //
225        // Currently layoutlib_create only overrides static native method.
226        // Static non-natives are not overridden and thus do not get here.
227        final String debug = System.getenv("DEBUG_LAYOUT");
228        if (debug != null && !debug.equals("0") && !debug.equals("false")) {
229
230            OverrideMethod.setDefaultListener(new MethodAdapter() {
231                @Override
232                public void onInvokeV(String signature, boolean isNative, Object caller) {
233                    sDefaultLog.error(null, "Missing Stub: " + signature +
234                            (isNative ? " (native)" : ""), null /*data*/);
235
236                    if (debug.equalsIgnoreCase("throw")) {
237                        // Throwing this exception doesn't seem that useful. It breaks
238                        // the layout editor yet doesn't display anything meaningful to the
239                        // user. Having the error in the console is just as useful. We'll
240                        // throw it only if the environment variable is "throw" or "THROW".
241                        throw new StaticMethodNotImplementedException(signature);
242                    }
243                }
244            });
245        }
246
247        // load the fonts.
248        FontLoader fontLoader = FontLoader.create(fontLocation.getAbsolutePath());
249        if (fontLoader != null) {
250            Typeface_Delegate.init(fontLoader);
251        } else {
252            log.error(LayoutLog.TAG_BROKEN,
253                    "Failed create FontLoader in layout lib.", null);
254            return false;
255        }
256
257        // now parse com.android.internal.R (and only this one as android.R is a subset of
258        // the internal version), and put the content in the maps.
259        try {
260            Class<?> r = com.android.internal.R.class;
261
262            for (Class<?> inner : r.getDeclaredClasses()) {
263                String resTypeName = inner.getSimpleName();
264                ResourceType resType = ResourceType.getEnum(resTypeName);
265                if (resType != null) {
266                    Map<String, Integer> fullMap = new HashMap<String, Integer>();
267                    sRevRMap.put(resType, fullMap);
268
269                    for (Field f : inner.getDeclaredFields()) {
270                        // only process static final fields. Since the final attribute may have
271                        // been altered by layoutlib_create, we only check static
272                        int modifiers = f.getModifiers();
273                        if (Modifier.isStatic(modifiers)) {
274                            Class<?> type = f.getType();
275                            if (type.isArray() && type.getComponentType() == int.class) {
276                                // if the object is an int[] we put it in sRArrayMap using an IntArray
277                                // wrapper that properly implements equals and hashcode for the array
278                                // objects, as required by the map contract.
279                                sRArrayMap.put(new IntArray((int[]) f.get(null)), f.getName());
280                            } else if (type == int.class) {
281                                Integer value = (Integer) f.get(null);
282                                sRMap.put(value, Pair.of(resType, f.getName()));
283                                fullMap.put(f.getName(), value);
284                            } else {
285                                assert false;
286                            }
287                        }
288                    }
289                }
290            }
291        } catch (Throwable throwable) {
292            if (log != null) {
293                log.error(LayoutLog.TAG_BROKEN,
294                        "Failed to load com.android.internal.R from the layout library jar",
295                        throwable);
296            }
297            return false;
298        }
299
300        return true;
301    }
302
303    @Override
304    public boolean dispose() {
305        BridgeAssetManager.clearSystem();
306
307        // dispose of the default typeface.
308        Typeface_Accessor.resetDefaults();
309
310        return true;
311    }
312
313    /**
314     * Starts a layout session by inflating and rendering it. The method returns a
315     * {@link RenderSession} on which further actions can be taken.
316     *
317     * @param params the {@link SessionParams} object with all the information necessary to create
318     *           the scene.
319     * @return a new {@link RenderSession} object that contains the result of the layout.
320     * @since 5
321     */
322    @Override
323    public RenderSession createSession(SessionParams params) {
324        try {
325            Result lastResult = SUCCESS.createResult();
326            RenderSessionImpl scene = new RenderSessionImpl(params);
327            try {
328                prepareThread();
329                lastResult = scene.init(params.getTimeout());
330                if (lastResult.isSuccess()) {
331                    lastResult = scene.inflate();
332                    if (lastResult.isSuccess()) {
333                        lastResult = scene.render(true /*freshRender*/);
334                    }
335                }
336            } finally {
337                scene.release();
338                cleanupThread();
339            }
340
341            return new BridgeRenderSession(scene, lastResult);
342        } catch (Throwable t) {
343            // get the real cause of the exception.
344            Throwable t2 = t;
345            while (t2.getCause() != null) {
346                t2 = t.getCause();
347            }
348            return new BridgeRenderSession(null,
349                    ERROR_UNKNOWN.createResult(t2.getMessage(), t));
350        }
351    }
352
353    @Override
354    public Result renderDrawable(DrawableParams params) {
355        try {
356            Result lastResult = SUCCESS.createResult();
357            RenderDrawable action = new RenderDrawable(params);
358            try {
359                prepareThread();
360                lastResult = action.init(params.getTimeout());
361                if (lastResult.isSuccess()) {
362                    lastResult = action.render();
363                }
364            } finally {
365                action.release();
366                cleanupThread();
367            }
368
369            return lastResult;
370        } catch (Throwable t) {
371            // get the real cause of the exception.
372            Throwable t2 = t;
373            while (t2.getCause() != null) {
374                t2 = t.getCause();
375            }
376            return ERROR_UNKNOWN.createResult(t2.getMessage(), t);
377        }
378    }
379
380    @Override
381    public void clearCaches(Object projectKey) {
382        if (projectKey != null) {
383            sProjectBitmapCache.remove(projectKey);
384            sProject9PatchCache.remove(projectKey);
385        }
386    }
387
388    @Override
389    public Result getViewParent(Object viewObject) {
390        if (viewObject instanceof View) {
391            return Status.SUCCESS.createResult(((View)viewObject).getParent());
392        }
393
394        throw new IllegalArgumentException("viewObject is not a View");
395    }
396
397    @Override
398    public Result getViewIndex(Object viewObject) {
399        if (viewObject instanceof View) {
400            View view = (View) viewObject;
401            ViewParent parentView = view.getParent();
402
403            if (parentView instanceof ViewGroup) {
404                Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view));
405            }
406
407            return Status.SUCCESS.createResult();
408        }
409
410        throw new IllegalArgumentException("viewObject is not a View");
411    }
412
413    /**
414     * Returns the lock for the bridge
415     */
416    public static ReentrantLock getLock() {
417        return sLock;
418    }
419
420    /**
421     * Prepares the current thread for rendering.
422     *
423     * Note that while this can be called several time, the first call to {@link #cleanupThread()}
424     * will do the clean-up, and make the thread unable to do further scene actions.
425     */
426    public static void prepareThread() {
427        // we need to make sure the Looper has been initialized for this thread.
428        // this is required for View that creates Handler objects.
429        if (Looper.myLooper() == null) {
430            Looper.prepare();
431        }
432    }
433
434    /**
435     * Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
436     * <p>
437     * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
438     * call to this will prevent the thread from doing further scene actions
439     */
440    public static void cleanupThread() {
441        // clean up the looper
442        Looper_Accessor.cleanupThread();
443    }
444
445    public static LayoutLog getLog() {
446        return sCurrentLog;
447    }
448
449    public static void setLog(LayoutLog log) {
450        // check only the thread currently owning the lock can do this.
451        if (sLock.isHeldByCurrentThread() == false) {
452            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
453        }
454
455        if (log != null) {
456            sCurrentLog = log;
457        } else {
458            sCurrentLog = sDefaultLog;
459        }
460    }
461
462    /**
463     * Returns details of a framework resource from its integer value.
464     * @param value the integer value
465     * @return a Pair containing the resource type and name, or null if the id
466     *     does not match any resource.
467     */
468    public static Pair<ResourceType, String> resolveResourceId(int value) {
469        Pair<ResourceType, String> pair = sRMap.get(value);
470        if (pair == null) {
471            pair = sDynamicIds.resolveId(value);
472            if (pair == null) {
473                //System.out.println(String.format("Missing id: %1$08X (%1$d)", value));
474            }
475        }
476        return pair;
477    }
478
479    /**
480     * Returns the name of a framework resource whose value is an int array.
481     * @param array
482     */
483    public static String resolveResourceId(int[] array) {
484        sIntArrayWrapper.set(array);
485        return sRArrayMap.get(sIntArrayWrapper);
486    }
487
488    /**
489     * Returns the integer id of a framework resource, from a given resource type and resource name.
490     * @param type the type of the resource
491     * @param name the name of the resource.
492     * @return an {@link Integer} containing the resource id, or null if no resource were found.
493     */
494    public static Integer getResourceId(ResourceType type, String name) {
495        Map<String, Integer> map = sRevRMap.get(type);
496        Integer value = null;
497        if (map != null) {
498            value = map.get(name);
499        }
500
501        if (value == null) {
502            value = sDynamicIds.getId(type, name);
503        }
504
505        return value;
506    }
507
508    /**
509     * Returns the list of possible enums for a given attribute name.
510     */
511    public static Map<String, Integer> getEnumValues(String attributeName) {
512        if (sEnumValueMap != null) {
513            return sEnumValueMap.get(attributeName);
514        }
515
516        return null;
517    }
518
519    /**
520     * Returns the platform build properties.
521     */
522    public static Map<String, String> getPlatformProperties() {
523        return sPlatformProperties;
524    }
525
526    /**
527     * Returns the bitmap for a specific path, from a specific project cache, or from the
528     * framework cache.
529     * @param value the path of the bitmap
530     * @param projectKey the key of the project, or null to query the framework cache.
531     * @return the cached Bitmap or null if not found.
532     */
533    public static Bitmap getCachedBitmap(String value, Object projectKey) {
534        if (projectKey != null) {
535            Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
536            if (map != null) {
537                SoftReference<Bitmap> ref = map.get(value);
538                if (ref != null) {
539                    return ref.get();
540                }
541            }
542        } else {
543            SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
544            if (ref != null) {
545                return ref.get();
546            }
547        }
548
549        return null;
550    }
551
552    /**
553     * Sets a bitmap in a project cache or in the framework cache.
554     * @param value the path of the bitmap
555     * @param bmp the Bitmap object
556     * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
557     */
558    public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
559        if (projectKey != null) {
560            Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
561
562            if (map == null) {
563                map = new HashMap<String, SoftReference<Bitmap>>();
564                sProjectBitmapCache.put(projectKey, map);
565            }
566
567            map.put(value, new SoftReference<Bitmap>(bmp));
568        } else {
569            sFrameworkBitmapCache.put(value, new SoftReference<Bitmap>(bmp));
570        }
571    }
572
573    /**
574     * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
575     * framework cache.
576     * @param value the path of the 9 patch
577     * @param projectKey the key of the project, or null to query the framework cache.
578     * @return the cached 9 patch or null if not found.
579     */
580    public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
581        if (projectKey != null) {
582            Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
583
584            if (map != null) {
585                SoftReference<NinePatchChunk> ref = map.get(value);
586                if (ref != null) {
587                    return ref.get();
588                }
589            }
590        } else {
591            SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
592            if (ref != null) {
593                return ref.get();
594            }
595        }
596
597        return null;
598    }
599
600    /**
601     * Sets a 9 patch chunk in a project cache or in the framework cache.
602     * @param value the path of the 9 patch
603     * @param ninePatch the 9 patch object
604     * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
605     */
606    public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
607        if (projectKey != null) {
608            Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
609
610            if (map == null) {
611                map = new HashMap<String, SoftReference<NinePatchChunk>>();
612                sProject9PatchCache.put(projectKey, map);
613            }
614
615            map.put(value, new SoftReference<NinePatchChunk>(ninePatch));
616        } else {
617            sFramework9PatchCache.put(value, new SoftReference<NinePatchChunk>(ninePatch));
618        }
619    }
620}
621