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 
17 package com.android.layoutlib.bridge;
18 
19 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
20 import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
21 
22 import com.android.annotations.NonNull;
23 import com.android.ide.common.rendering.api.Capability;
24 import com.android.ide.common.rendering.api.DrawableParams;
25 import com.android.ide.common.rendering.api.Features;
26 import com.android.ide.common.rendering.api.LayoutLog;
27 import com.android.ide.common.rendering.api.RenderSession;
28 import com.android.ide.common.rendering.api.Result;
29 import com.android.ide.common.rendering.api.Result.Status;
30 import com.android.ide.common.rendering.api.SessionParams;
31 import com.android.layoutlib.bridge.impl.RenderDrawable;
32 import com.android.layoutlib.bridge.impl.RenderSessionImpl;
33 import com.android.layoutlib.bridge.util.DynamicIdMap;
34 import com.android.ninepatch.NinePatchChunk;
35 import com.android.resources.ResourceType;
36 import com.android.tools.layoutlib.create.MethodAdapter;
37 import com.android.tools.layoutlib.create.OverrideMethod;
38 import com.android.util.Pair;
39 import com.ibm.icu.util.ULocale;
40 import libcore.io.MemoryMappedFile_Delegate;
41 
42 import android.content.res.BridgeAssetManager;
43 import android.graphics.Bitmap;
44 import android.graphics.FontFamily_Delegate;
45 import android.graphics.Typeface_Delegate;
46 import android.os.Looper;
47 import android.os.Looper_Accessor;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.view.ViewParent;
51 
52 import java.io.File;
53 import java.lang.ref.SoftReference;
54 import java.lang.reflect.Field;
55 import java.lang.reflect.Modifier;
56 import java.util.Arrays;
57 import java.util.EnumMap;
58 import java.util.EnumSet;
59 import java.util.HashMap;
60 import java.util.Map;
61 import 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  */
68 public 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 
StaticMethodNotImplementedException(String msg)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 
IntArray()128         private IntArray() {
129             // do nothing
130         }
131 
IntArray(int[] a)132         private IntArray(int[] a) {
133             mArray = a;
134         }
135 
set(int[] a)136         private void set(int[] a) {
137             mArray = a;
138         }
139 
140         @Override
hashCode()141         public int hashCode() {
142             return Arrays.hashCode(mArray);
143         }
144 
145         @Override
equals(Object obj)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
getApiLevel()187     public int getApiLevel() {
188         return com.android.ide.common.rendering.api.Bridge.API_CURRENT;
189     }
190 
191     @Override
192     @Deprecated
getCapabilities()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
supports(int feature)200     public boolean supports(int feature) {
201         return feature <= LAST_SUPPORTED_FEATURE;
202     }
203 
204     @Override
init(Map<String,String> platformProperties, File fontLocation, Map<String, Map<String, Integer>> enumValueMap, LayoutLog log)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
dispose()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
createSession(SessionParams params)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
renderDrawable(DrawableParams params)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
clearCaches(Object projectKey)371     public void clearCaches(Object projectKey) {
372         if (projectKey != null) {
373             sProjectBitmapCache.remove(projectKey);
374             sProject9PatchCache.remove(projectKey);
375         }
376     }
377 
378     @Override
getViewParent(Object viewObject)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
getViewIndex(Object viewObject)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
isRtl(String locale)404     public boolean isRtl(String locale) {
405         return isLocaleRtl(locale);
406     }
407 
isLocaleRtl(String locale)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      */
getLock()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      */
prepareThread()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      */
cleanupThread()443     public static void cleanupThread() {
444         // clean up the looper
445         Looper_Accessor.cleanupThread();
446     }
447 
getLog()448     public static LayoutLog getLog() {
449         return sCurrentLog;
450     }
451 
setLog(LayoutLog log)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      */
resolveResourceId(int value)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      */
resolveResourceId(int[] array)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
getResourceId(ResourceType type, String name)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      */
getEnumValues(String attributeName)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      */
getPlatformProperties()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      */
getCachedBitmap(String value, Object projectKey)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      */
setCachedBitmap(String value, Bitmap bmp, Object projectKey)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      */
getCached9Patch(String value, Object projectKey)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      */
setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey)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