Bridge.java revision 17a6170c62e6f74f2881623a9c16f0b6fea54721
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.annotations.NonNull;
23import com.android.ide.common.rendering.api.Capability;
24import com.android.ide.common.rendering.api.DrawableParams;
25import com.android.ide.common.rendering.api.LayoutLog;
26import com.android.ide.common.rendering.api.RenderSession;
27import com.android.ide.common.rendering.api.Result;
28import com.android.ide.common.rendering.api.Result.Status;
29import com.android.ide.common.rendering.api.SessionParams;
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;
39import libcore.io.MemoryMappedFile_Delegate;
40
41import android.content.res.BridgeAssetManager;
42import android.graphics.Bitmap;
43import android.graphics.FontFamily_Delegate;
44import android.graphics.Typeface_Delegate;
45import android.os.Looper;
46import android.os.Looper_Accessor;
47import android.view.View;
48import android.view.ViewGroup;
49import android.view.ViewParent;
50
51import java.io.File;
52import java.lang.ref.SoftReference;
53import java.lang.reflect.Field;
54import java.lang.reflect.Modifier;
55import java.util.Arrays;
56import java.util.EnumMap;
57import java.util.EnumSet;
58import java.util.HashMap;
59import java.util.Map;
60import java.util.concurrent.locks.ReentrantLock;
61
62/**
63 * Main entry point of the LayoutLib Bridge.
64 * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call
65 * {@link #createSession(SessionParams)}
66 */
67public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
68
69    private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left";
70
71    public static class StaticMethodNotImplementedException extends RuntimeException {
72        private static final long serialVersionUID = 1L;
73
74        public StaticMethodNotImplementedException(String msg) {
75            super(msg);
76        }
77    }
78
79    /**
80     * Lock to ensure only one rendering/inflating happens at a time.
81     * This is due to some singleton in the Android framework.
82     */
83    private final static ReentrantLock sLock = new ReentrantLock();
84
85    /**
86     * Maps from id to resource type/name. This is for com.android.internal.R
87     */
88    private final static Map<Integer, Pair<ResourceType, String>> sRMap =
89        new HashMap<Integer, Pair<ResourceType, String>>();
90
91    /**
92     * Same as sRMap except for int[] instead of int resources. This is for android.R only.
93     */
94    private final static Map<IntArray, String> sRArrayMap = new HashMap<IntArray, String>();
95    /**
96     * Reverse map compared to sRMap, resource type -> (resource name -> id).
97     * This is for com.android.internal.R.
98     */
99    private final static Map<ResourceType, Map<String, Integer>> sRevRMap =
100        new EnumMap<ResourceType, Map<String,Integer>>(ResourceType.class);
101
102    // framework resources are defined as 0x01XX#### where XX is the resource type (layout,
103    // drawable, etc...). Using FF as the type allows for 255 resource types before we get a
104    // collision which should be fine.
105    private final static int DYNAMIC_ID_SEED_START = 0x01ff0000;
106    private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START);
107
108    private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache =
109        new HashMap<Object, Map<String, SoftReference<Bitmap>>>();
110    private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache =
111        new HashMap<Object, Map<String, SoftReference<NinePatchChunk>>>();
112
113    private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache =
114        new HashMap<String, SoftReference<Bitmap>>();
115    private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache =
116        new HashMap<String, SoftReference<NinePatchChunk>>();
117
118    private static Map<String, Map<String, Integer>> sEnumValueMap;
119    private static Map<String, String> sPlatformProperties;
120
121    /**
122     * int[] wrapper to use as keys in maps.
123     */
124    private final static class IntArray {
125        private int[] mArray;
126
127        private IntArray() {
128            // do nothing
129        }
130
131        private IntArray(int[] a) {
132            mArray = a;
133        }
134
135        private void set(int[] a) {
136            mArray = a;
137        }
138
139        @Override
140        public int hashCode() {
141            return Arrays.hashCode(mArray);
142        }
143
144        @Override
145        public boolean equals(Object obj) {
146            if (this == obj) return true;
147            if (obj == null) return false;
148            if (getClass() != obj.getClass()) return false;
149
150            IntArray other = (IntArray) obj;
151            return Arrays.equals(mArray, other.mArray);
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                Capability.ACTION_BAR,
220                Capability.SIMULATE_PLATFORM);
221
222
223        BridgeAssetManager.initSystem();
224
225        // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener
226        // on static (native) methods which prints the signature on the console and
227        // throws an exception.
228        // This is useful when testing the rendering in ADT to identify static native
229        // methods that are ignored -- layoutlib_create makes them returns 0/false/null
230        // which is generally OK yet might be a problem, so this is how you'd find out.
231        //
232        // Currently layoutlib_create only overrides static native method.
233        // Static non-natives are not overridden and thus do not get here.
234        final String debug = System.getenv("DEBUG_LAYOUT");
235        if (debug != null && !debug.equals("0") && !debug.equals("false")) {
236
237            OverrideMethod.setDefaultListener(new MethodAdapter() {
238                @Override
239                public void onInvokeV(String signature, boolean isNative, Object caller) {
240                    sDefaultLog.error(null, "Missing Stub: " + signature +
241                            (isNative ? " (native)" : ""), null /*data*/);
242
243                    if (debug.equalsIgnoreCase("throw")) {
244                        // Throwing this exception doesn't seem that useful. It breaks
245                        // the layout editor yet doesn't display anything meaningful to the
246                        // user. Having the error in the console is just as useful. We'll
247                        // throw it only if the environment variable is "throw" or "THROW".
248                        throw new StaticMethodNotImplementedException(signature);
249                    }
250                }
251            });
252        }
253
254        // load the fonts.
255        FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath());
256        MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile());
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, null);
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_Delegate.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    @Override
415    public boolean isRtl(String locale) {
416        return isLocaleRtl(locale);
417    }
418
419    public static boolean isLocaleRtl(String locale) {
420        if (locale == null) {
421            locale = "";
422        }
423        ULocale uLocale = new ULocale(locale);
424        return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL);
425    }
426
427    /**
428     * Returns the lock for the bridge
429     */
430    public static ReentrantLock getLock() {
431        return sLock;
432    }
433
434    /**
435     * Prepares the current thread for rendering.
436     *
437     * Note that while this can be called several time, the first call to {@link #cleanupThread()}
438     * will do the clean-up, and make the thread unable to do further scene actions.
439     */
440    public static void prepareThread() {
441        // we need to make sure the Looper has been initialized for this thread.
442        // this is required for View that creates Handler objects.
443        if (Looper.myLooper() == null) {
444            Looper.prepareMainLooper();
445        }
446    }
447
448    /**
449     * Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
450     * <p>
451     * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
452     * call to this will prevent the thread from doing further scene actions
453     */
454    public static void cleanupThread() {
455        // clean up the looper
456        Looper_Accessor.cleanupThread();
457    }
458
459    public static LayoutLog getLog() {
460        return sCurrentLog;
461    }
462
463    public static void setLog(LayoutLog log) {
464        // check only the thread currently owning the lock can do this.
465        if (!sLock.isHeldByCurrentThread()) {
466            throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
467        }
468
469        if (log != null) {
470            sCurrentLog = log;
471        } else {
472            sCurrentLog = sDefaultLog;
473        }
474    }
475
476    /**
477     * Returns details of a framework resource from its integer value.
478     * @param value the integer value
479     * @return a Pair containing the resource type and name, or null if the id
480     *     does not match any resource.
481     */
482    public static Pair<ResourceType, String> resolveResourceId(int value) {
483        Pair<ResourceType, String> pair = sRMap.get(value);
484        if (pair == null) {
485            pair = sDynamicIds.resolveId(value);
486            if (pair == null) {
487                //System.out.println(String.format("Missing id: %1$08X (%1$d)", value));
488            }
489        }
490        return pair;
491    }
492
493    /**
494     * Returns the name of a framework resource whose value is an int array.
495     */
496    public static String resolveResourceId(int[] array) {
497        sIntArrayWrapper.set(array);
498        return sRArrayMap.get(sIntArrayWrapper);
499    }
500
501    /**
502     * Returns the integer id of a framework resource, from a given resource type and resource name.
503     * @param type the type of the resource
504     * @param name the name of the resource.
505     * @return an {@link Integer} containing the resource id, or null if no resource were found.
506     */
507    @NonNull
508    public static Integer getResourceId(ResourceType type, String name) {
509        Map<String, Integer> map = sRevRMap.get(type);
510        Integer value = null;
511        if (map != null) {
512            value = map.get(name);
513        }
514
515        return value == null ? sDynamicIds.getId(type, name) : value;
516
517    }
518
519    /**
520     * Returns the list of possible enums for a given attribute name.
521     */
522    public static Map<String, Integer> getEnumValues(String attributeName) {
523        if (sEnumValueMap != null) {
524            return sEnumValueMap.get(attributeName);
525        }
526
527        return null;
528    }
529
530    /**
531     * Returns the platform build properties.
532     */
533    public static Map<String, String> getPlatformProperties() {
534        return sPlatformProperties;
535    }
536
537    /**
538     * Returns the bitmap for a specific path, from a specific project cache, or from the
539     * framework cache.
540     * @param value the path of the bitmap
541     * @param projectKey the key of the project, or null to query the framework cache.
542     * @return the cached Bitmap or null if not found.
543     */
544    public static Bitmap getCachedBitmap(String value, Object projectKey) {
545        if (projectKey != null) {
546            Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
547            if (map != null) {
548                SoftReference<Bitmap> ref = map.get(value);
549                if (ref != null) {
550                    return ref.get();
551                }
552            }
553        } else {
554            SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
555            if (ref != null) {
556                return ref.get();
557            }
558        }
559
560        return null;
561    }
562
563    /**
564     * Sets a bitmap in a project cache or in the framework cache.
565     * @param value the path of the bitmap
566     * @param bmp the Bitmap object
567     * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
568     */
569    public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
570        if (projectKey != null) {
571            Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
572
573            if (map == null) {
574                map = new HashMap<String, SoftReference<Bitmap>>();
575                sProjectBitmapCache.put(projectKey, map);
576            }
577
578            map.put(value, new SoftReference<Bitmap>(bmp));
579        } else {
580            sFrameworkBitmapCache.put(value, new SoftReference<Bitmap>(bmp));
581        }
582    }
583
584    /**
585     * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
586     * framework cache.
587     * @param value the path of the 9 patch
588     * @param projectKey the key of the project, or null to query the framework cache.
589     * @return the cached 9 patch or null if not found.
590     */
591    public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
592        if (projectKey != null) {
593            Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
594
595            if (map != null) {
596                SoftReference<NinePatchChunk> ref = map.get(value);
597                if (ref != null) {
598                    return ref.get();
599                }
600            }
601        } else {
602            SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
603            if (ref != null) {
604                return ref.get();
605            }
606        }
607
608        return null;
609    }
610
611    /**
612     * Sets a 9 patch chunk in a project cache or in the framework cache.
613     * @param value the path of the 9 patch
614     * @param ninePatch the 9 patch object
615     * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
616     */
617    public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
618        if (projectKey != null) {
619            Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
620
621            if (map == null) {
622                map = new HashMap<String, SoftReference<NinePatchChunk>>();
623                sProject9PatchCache.put(projectKey, map);
624            }
625
626            map.put(value, new SoftReference<NinePatchChunk>(ninePatch));
627        } else {
628            sFramework9PatchCache.put(value, new SoftReference<NinePatchChunk>(ninePatch));
629        }
630    }
631}
632