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                Capability.FIXED_SCALABLE_NINE_PATCH);
215
216
217        BridgeAssetManager.initSystem();
218
219        // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener
220        // on static (native) methods which prints the signature on the console and
221        // throws an exception.
222        // This is useful when testing the rendering in ADT to identify static native
223        // methods that are ignored -- layoutlib_create makes them returns 0/false/null
224        // which is generally OK yet might be a problem, so this is how you'd find out.
225        //
226        // Currently layoutlib_create only overrides static native method.
227        // Static non-natives are not overridden and thus do not get here.
228        final String debug = System.getenv("DEBUG_LAYOUT");
229        if (debug != null && !debug.equals("0") && !debug.equals("false")) {
230
231            OverrideMethod.setDefaultListener(new MethodAdapter() {
232                @Override
233                public void onInvokeV(String signature, boolean isNative, Object caller) {
234                    sDefaultLog.error(null, "Missing Stub: " + signature +
235                            (isNative ? " (native)" : ""), null /*data*/);
236
237                    if (debug.equalsIgnoreCase("throw")) {
238                        // Throwing this exception doesn't seem that useful. It breaks
239                        // the layout editor yet doesn't display anything meaningful to the
240                        // user. Having the error in the console is just as useful. We'll
241                        // throw it only if the environment variable is "throw" or "THROW".
242                        throw new StaticMethodNotImplementedException(signature);
243                    }
244                }
245            });
246        }
247
248        // load the fonts.
249        FontLoader fontLoader = FontLoader.create(fontLocation.getAbsolutePath());
250        if (fontLoader != null) {
251            Typeface_Delegate.init(fontLoader);
252        } else {
253            log.error(LayoutLog.TAG_BROKEN,
254                    "Failed create FontLoader in layout lib.", null);
255            return false;
256        }
257
258        // now parse com.android.internal.R (and only this one as android.R is a subset of
259        // the internal version), and put the content in the maps.
260        try {
261            Class<?> r = com.android.internal.R.class;
262
263            for (Class<?> inner : r.getDeclaredClasses()) {
264                String resTypeName = inner.getSimpleName();
265                ResourceType resType = ResourceType.getEnum(resTypeName);
266                if (resType != null) {
267                    Map<String, Integer> fullMap = new HashMap<String, Integer>();
268                    sRevRMap.put(resType, fullMap);
269
270                    for (Field f : inner.getDeclaredFields()) {
271                        // only process static final fields. Since the final attribute may have
272                        // been altered by layoutlib_create, we only check static
273                        int modifiers = f.getModifiers();
274                        if (Modifier.isStatic(modifiers)) {
275                            Class<?> type = f.getType();
276                            if (type.isArray() && type.getComponentType() == int.class) {
277                                // if the object is an int[] we put it in sRArrayMap using an IntArray
278                                // wrapper that properly implements equals and hashcode for the array
279                                // objects, as required by the map contract.
280                                sRArrayMap.put(new IntArray((int[]) f.get(null)), f.getName());
281                            } else if (type == int.class) {
282                                Integer value = (Integer) f.get(null);
283                                sRMap.put(value, Pair.of(resType, f.getName()));
284                                fullMap.put(f.getName(), value);
285                            } else {
286                                assert false;
287                            }
288                        }
289                    }
290                }
291            }
292        } catch (Throwable throwable) {
293            if (log != null) {
294                log.error(LayoutLog.TAG_BROKEN,
295                        "Failed to load com.android.internal.R from the layout library jar",
296                        throwable);
297            }
298            return false;
299        }
300
301        return true;
302    }
303
304    @Override
305    public boolean dispose() {
306        BridgeAssetManager.clearSystem();
307
308        // dispose of the default typeface.
309        Typeface_Accessor.resetDefaults();
310
311        return true;
312    }
313
314    /**
315     * Starts a layout session by inflating and rendering it. The method returns a
316     * {@link RenderSession} on which further actions can be taken.
317     *
318     * @param params the {@link SessionParams} object with all the information necessary to create
319     *           the scene.
320     * @return a new {@link RenderSession} object that contains the result of the layout.
321     * @since 5
322     */
323    @Override
324    public RenderSession createSession(SessionParams params) {
325        try {
326            Result lastResult = SUCCESS.createResult();
327            RenderSessionImpl scene = new RenderSessionImpl(params);
328            try {
329                prepareThread();
330                lastResult = scene.init(params.getTimeout());
331                if (lastResult.isSuccess()) {
332                    lastResult = scene.inflate();
333                    if (lastResult.isSuccess()) {
334                        lastResult = scene.render(true /*freshRender*/);
335                    }
336                }
337            } finally {
338                scene.release();
339                cleanupThread();
340            }
341
342            return new BridgeRenderSession(scene, lastResult);
343        } catch (Throwable t) {
344            // get the real cause of the exception.
345            Throwable t2 = t;
346            while (t2.getCause() != null) {
347                t2 = t.getCause();
348            }
349            return new BridgeRenderSession(null,
350                    ERROR_UNKNOWN.createResult(t2.getMessage(), t));
351        }
352    }
353
354    @Override
355    public Result renderDrawable(DrawableParams params) {
356        try {
357            Result lastResult = SUCCESS.createResult();
358            RenderDrawable action = new RenderDrawable(params);
359            try {
360                prepareThread();
361                lastResult = action.init(params.getTimeout());
362                if (lastResult.isSuccess()) {
363                    lastResult = action.render();
364                }
365            } finally {
366                action.release();
367                cleanupThread();
368            }
369
370            return lastResult;
371        } catch (Throwable t) {
372            // get the real cause of the exception.
373            Throwable t2 = t;
374            while (t2.getCause() != null) {
375                t2 = t.getCause();
376            }
377            return ERROR_UNKNOWN.createResult(t2.getMessage(), t);
378        }
379    }
380
381    @Override
382    public void clearCaches(Object projectKey) {
383        if (projectKey != null) {
384            sProjectBitmapCache.remove(projectKey);
385            sProject9PatchCache.remove(projectKey);
386        }
387    }
388
389    @Override
390    public Result getViewParent(Object viewObject) {
391        if (viewObject instanceof View) {
392            return Status.SUCCESS.createResult(((View)viewObject).getParent());
393        }
394
395        throw new IllegalArgumentException("viewObject is not a View");
396    }
397
398    @Override
399    public Result getViewIndex(Object viewObject) {
400        if (viewObject instanceof View) {
401            View view = (View) viewObject;
402            ViewParent parentView = view.getParent();
403
404            if (parentView instanceof ViewGroup) {
405                Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view));
406            }
407
408            return Status.SUCCESS.createResult();
409        }
410
411        throw new IllegalArgumentException("viewObject is not a View");
412    }
413
414    /**
415     * Returns the lock for the bridge
416     */
417    public static ReentrantLock getLock() {
418        return sLock;
419    }
420
421    /**
422     * Prepares the current thread for rendering.
423     *
424     * Note that while this can be called several time, the first call to {@link #cleanupThread()}
425     * will do the clean-up, and make the thread unable to do further scene actions.
426     */
427    public static void prepareThread() {
428        // we need to make sure the Looper has been initialized for this thread.
429        // this is required for View that creates Handler objects.
430        if (Looper.myLooper() == null) {
431            Looper.prepare();
432        }
433    }
434
435    /**
436     * Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
437     * <p>
438     * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
439     * call to this will prevent the thread from doing further scene actions
440     */
441    public static void cleanupThread() {
442        // clean up the looper
443        Looper_Accessor.cleanupThread();
444    }
445
446    public static LayoutLog getLog() {
447        return sCurrentLog;
448    }
449
450    public static void setLog(LayoutLog log) {
451        // check only the thread currently owning the lock can do this.
452        if (sLock.isHeldByCurrentThread() == false) {
453            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
454        }
455
456        if (log != null) {
457            sCurrentLog = log;
458        } else {
459            sCurrentLog = sDefaultLog;
460        }
461    }
462
463    /**
464     * Returns details of a framework resource from its integer value.
465     * @param value the integer value
466     * @return a Pair containing the resource type and name, or null if the id
467     *     does not match any resource.
468     */
469    public static Pair<ResourceType, String> resolveResourceId(int value) {
470        Pair<ResourceType, String> pair = sRMap.get(value);
471        if (pair == null) {
472            pair = sDynamicIds.resolveId(value);
473            if (pair == null) {
474                //System.out.println(String.format("Missing id: %1$08X (%1$d)", value));
475            }
476        }
477        return pair;
478    }
479
480    /**
481     * Returns the name of a framework resource whose value is an int array.
482     * @param array
483     */
484    public static String resolveResourceId(int[] array) {
485        sIntArrayWrapper.set(array);
486        return sRArrayMap.get(sIntArrayWrapper);
487    }
488
489    /**
490     * Returns the integer id of a framework resource, from a given resource type and resource name.
491     * @param type the type of the resource
492     * @param name the name of the resource.
493     * @return an {@link Integer} containing the resource id, or null if no resource were found.
494     */
495    public static Integer getResourceId(ResourceType type, String name) {
496        Map<String, Integer> map = sRevRMap.get(type);
497        Integer value = null;
498        if (map != null) {
499            value = map.get(name);
500        }
501
502        if (value == null) {
503            value = sDynamicIds.getId(type, name);
504        }
505
506        return value;
507    }
508
509    /**
510     * Returns the list of possible enums for a given attribute name.
511     */
512    public static Map<String, Integer> getEnumValues(String attributeName) {
513        if (sEnumValueMap != null) {
514            return sEnumValueMap.get(attributeName);
515        }
516
517        return null;
518    }
519
520    /**
521     * Returns the platform build properties.
522     */
523    public static Map<String, String> getPlatformProperties() {
524        return sPlatformProperties;
525    }
526
527    /**
528     * Returns the bitmap for a specific path, from a specific project cache, or from the
529     * framework cache.
530     * @param value the path of the bitmap
531     * @param projectKey the key of the project, or null to query the framework cache.
532     * @return the cached Bitmap or null if not found.
533     */
534    public static Bitmap getCachedBitmap(String value, Object projectKey) {
535        if (projectKey != null) {
536            Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
537            if (map != null) {
538                SoftReference<Bitmap> ref = map.get(value);
539                if (ref != null) {
540                    return ref.get();
541                }
542            }
543        } else {
544            SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
545            if (ref != null) {
546                return ref.get();
547            }
548        }
549
550        return null;
551    }
552
553    /**
554     * Sets a bitmap in a project cache or in the framework cache.
555     * @param value the path of the bitmap
556     * @param bmp the Bitmap object
557     * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
558     */
559    public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
560        if (projectKey != null) {
561            Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
562
563            if (map == null) {
564                map = new HashMap<String, SoftReference<Bitmap>>();
565                sProjectBitmapCache.put(projectKey, map);
566            }
567
568            map.put(value, new SoftReference<Bitmap>(bmp));
569        } else {
570            sFrameworkBitmapCache.put(value, new SoftReference<Bitmap>(bmp));
571        }
572    }
573
574    /**
575     * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
576     * framework cache.
577     * @param value the path of the 9 patch
578     * @param projectKey the key of the project, or null to query the framework cache.
579     * @return the cached 9 patch or null if not found.
580     */
581    public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
582        if (projectKey != null) {
583            Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
584
585            if (map != null) {
586                SoftReference<NinePatchChunk> ref = map.get(value);
587                if (ref != null) {
588                    return ref.get();
589                }
590            }
591        } else {
592            SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
593            if (ref != null) {
594                return ref.get();
595            }
596        }
597
598        return null;
599    }
600
601    /**
602     * Sets a 9 patch chunk in a project cache or in the framework cache.
603     * @param value the path of the 9 patch
604     * @param ninePatch the 9 patch object
605     * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
606     */
607    public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
608        if (projectKey != null) {
609            Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
610
611            if (map == null) {
612                map = new HashMap<String, SoftReference<NinePatchChunk>>();
613                sProject9PatchCache.put(projectKey, map);
614            }
615
616            map.put(value, new SoftReference<NinePatchChunk>(ninePatch));
617        } else {
618            sFramework9PatchCache.put(value, new SoftReference<NinePatchChunk>(ninePatch));
619        }
620    }
621}
622