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