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