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 com.android.ide.common.rendering.api.Capability;
20 import com.android.ide.common.rendering.api.DrawableParams;
21 import com.android.ide.common.rendering.api.Features;
22 import com.android.ide.common.rendering.api.LayoutLog;
23 import com.android.ide.common.rendering.api.RenderSession;
24 import com.android.ide.common.rendering.api.Result;
25 import com.android.ide.common.rendering.api.Result.Status;
26 import com.android.ide.common.rendering.api.SessionParams;
27 import com.android.layoutlib.bridge.android.RenderParamsFlags;
28 import com.android.layoutlib.bridge.impl.RenderDrawable;
29 import com.android.layoutlib.bridge.impl.RenderSessionImpl;
30 import com.android.layoutlib.bridge.util.DynamicIdMap;
31 import com.android.ninepatch.NinePatchChunk;
32 import com.android.resources.ResourceType;
33 import com.android.tools.layoutlib.create.MethodAdapter;
34 import com.android.tools.layoutlib.create.OverrideMethod;
35 import com.android.util.Pair;
36 
37 import android.annotation.NonNull;
38 import android.content.res.BridgeAssetManager;
39 import android.graphics.Bitmap;
40 import android.graphics.FontFamily_Delegate;
41 import android.graphics.Typeface_Delegate;
42 import android.icu.util.ULocale;
43 import android.os.Looper;
44 import android.os.Looper_Accessor;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.view.ViewParent;
48 
49 import java.io.File;
50 import java.lang.ref.SoftReference;
51 import java.lang.reflect.Field;
52 import java.lang.reflect.Modifier;
53 import java.util.Arrays;
54 import java.util.Comparator;
55 import java.util.EnumMap;
56 import java.util.EnumSet;
57 import java.util.HashMap;
58 import java.util.Map;
59 import java.util.concurrent.locks.ReentrantLock;
60 
61 import libcore.io.MemoryMappedFile_Delegate;
62 
63 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
64 import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
65 
66 /**
67  * Main entry point of the LayoutLib Bridge.
68  * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call
69  * {@link #createSession(SessionParams)}
70  */
71 public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
72 
73     private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left";
74 
75     public static class StaticMethodNotImplementedException extends RuntimeException {
76         private static final long serialVersionUID = 1L;
77 
StaticMethodNotImplementedException(String msg)78         public StaticMethodNotImplementedException(String msg) {
79             super(msg);
80         }
81     }
82 
83     /**
84      * Lock to ensure only one rendering/inflating happens at a time.
85      * This is due to some singleton in the Android framework.
86      */
87     private final static ReentrantLock sLock = new ReentrantLock();
88 
89     /**
90      * Maps from id to resource type/name. This is for com.android.internal.R
91      */
92     private final static Map<Integer, Pair<ResourceType, String>> sRMap =
93         new HashMap<Integer, Pair<ResourceType, String>>();
94 
95     /**
96      * Same as sRMap except for int[] instead of int resources. This is for android.R only.
97      */
98     private final static Map<IntArray, String> sRArrayMap = new HashMap<IntArray, String>(384);
99     /**
100      * Reverse map compared to sRMap, resource type -> (resource name -> id).
101      * This is for com.android.internal.R.
102      */
103     private final static Map<ResourceType, Map<String, Integer>> sRevRMap =
104         new EnumMap<ResourceType, Map<String,Integer>>(ResourceType.class);
105 
106     // framework resources are defined as 0x01XX#### where XX is the resource type (layout,
107     // drawable, etc...). Using FF as the type allows for 255 resource types before we get a
108     // collision which should be fine.
109     private final static int DYNAMIC_ID_SEED_START = 0x01ff0000;
110     private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START);
111 
112     private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache =
113         new HashMap<Object, Map<String, SoftReference<Bitmap>>>();
114     private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache =
115         new HashMap<Object, Map<String, SoftReference<NinePatchChunk>>>();
116 
117     private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache =
118         new HashMap<String, SoftReference<Bitmap>>();
119     private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache =
120         new HashMap<String, SoftReference<NinePatchChunk>>();
121 
122     private static Map<String, Map<String, Integer>> sEnumValueMap;
123     private static Map<String, String> sPlatformProperties;
124 
125     /**
126      * int[] wrapper to use as keys in maps.
127      */
128     private final static class IntArray {
129         private int[] mArray;
130 
IntArray()131         private IntArray() {
132             // do nothing
133         }
134 
IntArray(int[] a)135         private IntArray(int[] a) {
136             mArray = a;
137         }
138 
set(int[] a)139         private void set(int[] a) {
140             mArray = a;
141         }
142 
143         @Override
hashCode()144         public int hashCode() {
145             return Arrays.hashCode(mArray);
146         }
147 
148         @Override
equals(Object obj)149         public boolean equals(Object obj) {
150             if (this == obj) return true;
151             if (obj == null) return false;
152             if (getClass() != obj.getClass()) return false;
153 
154             IntArray other = (IntArray) obj;
155             return Arrays.equals(mArray, other.mArray);
156         }
157     }
158 
159     /** Instance of IntArrayWrapper to be reused in {@link #resolveResourceId(int[])}. */
160     private final static IntArray sIntArrayWrapper = new IntArray();
161 
162     /**
163      * A default log than prints to stdout/stderr.
164      */
165     private final static LayoutLog sDefaultLog = new LayoutLog() {
166         @Override
167         public void error(String tag, String message, Object data) {
168             System.err.println(message);
169         }
170 
171         @Override
172         public void error(String tag, String message, Throwable throwable, Object data) {
173             System.err.println(message);
174         }
175 
176         @Override
177         public void warning(String tag, String message, Object data) {
178             System.out.println(message);
179         }
180     };
181 
182     /**
183      * Current log.
184      */
185     private static LayoutLog sCurrentLog = sDefaultLog;
186 
187     private static final int LAST_SUPPORTED_FEATURE = Features.THEME_PREVIEW_NAVIGATION_BAR;
188 
189     @Override
getApiLevel()190     public int getApiLevel() {
191         return com.android.ide.common.rendering.api.Bridge.API_CURRENT;
192     }
193 
194     @Override
195     @Deprecated
getCapabilities()196     public EnumSet<Capability> getCapabilities() {
197         // The Capability class is deprecated and frozen. All Capabilities enumerated there are
198         // supported by this version of LayoutLibrary. So, it's safe to use EnumSet.allOf()
199         return EnumSet.allOf(Capability.class);
200     }
201 
202     @Override
supports(int feature)203     public boolean supports(int feature) {
204         return feature <= LAST_SUPPORTED_FEATURE;
205     }
206 
207     @Override
init(Map<String,String> platformProperties, File fontLocation, Map<String, Map<String, Integer>> enumValueMap, LayoutLog log)208     public boolean init(Map<String,String> platformProperties,
209             File fontLocation,
210             Map<String, Map<String, Integer>> enumValueMap,
211             LayoutLog log) {
212         sPlatformProperties = platformProperties;
213         sEnumValueMap = enumValueMap;
214 
215         BridgeAssetManager.initSystem();
216 
217         // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener
218         // on static (native) methods which prints the signature on the console and
219         // throws an exception.
220         // This is useful when testing the rendering in ADT to identify static native
221         // methods that are ignored -- layoutlib_create makes them returns 0/false/null
222         // which is generally OK yet might be a problem, so this is how you'd find out.
223         //
224         // Currently layoutlib_create only overrides static native method.
225         // Static non-natives are not overridden and thus do not get here.
226         final String debug = System.getenv("DEBUG_LAYOUT");
227         if (debug != null && !debug.equals("0") && !debug.equals("false")) {
228 
229             OverrideMethod.setDefaultListener(new MethodAdapter() {
230                 @Override
231                 public void onInvokeV(String signature, boolean isNative, Object caller) {
232                     sDefaultLog.error(null, "Missing Stub: " + signature +
233                             (isNative ? " (native)" : ""), null /*data*/);
234 
235                     if (debug.equalsIgnoreCase("throw")) {
236                         // Throwing this exception doesn't seem that useful. It breaks
237                         // the layout editor yet doesn't display anything meaningful to the
238                         // user. Having the error in the console is just as useful. We'll
239                         // throw it only if the environment variable is "throw" or "THROW".
240                         throw new StaticMethodNotImplementedException(signature);
241                     }
242                 }
243             });
244         }
245 
246         // load the fonts.
247         FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath());
248         MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile());
249 
250         // now parse com.android.internal.R (and only this one as android.R is a subset of
251         // the internal version), and put the content in the maps.
252         try {
253             Class<?> r = com.android.internal.R.class;
254             // Parse the styleable class first, since it may contribute to attr values.
255             parseStyleable();
256 
257             for (Class<?> inner : r.getDeclaredClasses()) {
258                 if (inner == com.android.internal.R.styleable.class) {
259                     // Already handled the styleable case. Not skipping attr, as there may be attrs
260                     // that are not referenced from styleables.
261                     continue;
262                 }
263                 String resTypeName = inner.getSimpleName();
264                 ResourceType resType = ResourceType.getEnum(resTypeName);
265                 if (resType != null) {
266                     Map<String, Integer> fullMap = null;
267                     switch (resType) {
268                         case ATTR:
269                             fullMap = sRevRMap.get(ResourceType.ATTR);
270                             break;
271                         case STRING:
272                         case STYLE:
273                             // Slightly less than thousand entries in each.
274                             fullMap = new HashMap<String, Integer>(1280);
275                             // no break.
276                         default:
277                             if (fullMap == null) {
278                                 fullMap = new HashMap<String, Integer>();
279                             }
280                             sRevRMap.put(resType, fullMap);
281                     }
282 
283                     for (Field f : inner.getDeclaredFields()) {
284                         // only process static final fields. Since the final attribute may have
285                         // been altered by layoutlib_create, we only check static
286                         if (!isValidRField(f)) {
287                             continue;
288                         }
289                         Class<?> type = f.getType();
290                         if (type.isArray()) {
291                             // if the object is an int[] we put it in sRArrayMap using an IntArray
292                             // wrapper that properly implements equals and hashcode for the array
293                             // objects, as required by the map contract.
294                             sRArrayMap.put(new IntArray((int[]) f.get(null)), f.getName());
295                         } else {
296                             Integer value = (Integer) f.get(null);
297                             sRMap.put(value, Pair.of(resType, f.getName()));
298                             fullMap.put(f.getName(), value);
299                         }
300                     }
301                 }
302             }
303         } catch (Exception throwable) {
304             if (log != null) {
305                 log.error(LayoutLog.TAG_BROKEN,
306                         "Failed to load com.android.internal.R from the layout library jar",
307                         throwable, null);
308             }
309             return false;
310         }
311 
312         return true;
313     }
314 
315     /**
316      * Tests if the field is pubic, static and one of int or int[].
317      */
isValidRField(Field field)318     private static boolean isValidRField(Field field) {
319         int modifiers = field.getModifiers();
320         boolean isAcceptable = Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
321         Class<?> type = field.getType();
322         return isAcceptable && type == int.class ||
323                 (type.isArray() && type.getComponentType() == int.class);
324 
325     }
326 
parseStyleable()327     private static void parseStyleable() throws Exception {
328         // R.attr doesn't contain all the needed values. There are too many resources in the
329         // framework for all to be in the R class. Only the ones specified manually in
330         // res/values/symbols.xml are put in R class. Since, we need to create a map of all attr
331         // values, we try and find them from the styleables.
332 
333         // There were 1500 elements in this map at M timeframe.
334         Map<String, Integer> revRAttrMap = new HashMap<String, Integer>(2048);
335         sRevRMap.put(ResourceType.ATTR, revRAttrMap);
336         // There were 2000 elements in this map at M timeframe.
337         Map<String, Integer> revRStyleableMap = new HashMap<String, Integer>(3072);
338         sRevRMap.put(ResourceType.STYLEABLE, revRStyleableMap);
339         Class<?> c = com.android.internal.R.styleable.class;
340         Field[] fields = c.getDeclaredFields();
341         // Sort the fields to bring all arrays to the beginning, so that indices into the array are
342         // able to refer back to the arrays (i.e. no forward references).
343         Arrays.sort(fields, new Comparator<Field>() {
344             @Override
345             public int compare(Field o1, Field o2) {
346                 if (o1 == o2) {
347                     return 0;
348                 }
349                 Class<?> t1 = o1.getType();
350                 Class<?> t2 = o2.getType();
351                 if (t1.isArray() && !t2.isArray()) {
352                     return -1;
353                 } else if (t2.isArray() && !t1.isArray()) {
354                     return 1;
355                 }
356                 return o1.getName().compareTo(o2.getName());
357             }
358         });
359         Map<String, int[]> styleables = new HashMap<String, int[]>();
360         for (Field field : fields) {
361             if (!isValidRField(field)) {
362                 // Only consider public static fields that are int or int[].
363                 // Don't check the final flag as it may have been modified by layoutlib_create.
364                 continue;
365             }
366             String name = field.getName();
367             if (field.getType().isArray()) {
368                 int[] styleableValue = (int[]) field.get(null);
369                 sRArrayMap.put(new IntArray(styleableValue), name);
370                 styleables.put(name, styleableValue);
371                 continue;
372             }
373             // Not an array.
374             String arrayName = name;
375             int[] arrayValue = null;
376             int index;
377             while ((index = arrayName.lastIndexOf('_')) >= 0) {
378                 // Find the name of the corresponding styleable.
379                 // Search in reverse order so that attrs like LinearLayout_Layout_layout_gravity
380                 // are mapped to LinearLayout_Layout and not to LinearLayout.
381                 arrayName = arrayName.substring(0, index);
382                 arrayValue = styleables.get(arrayName);
383                 if (arrayValue != null) {
384                     break;
385                 }
386             }
387             index = (Integer) field.get(null);
388             if (arrayValue != null) {
389                 String attrName = name.substring(arrayName.length() + 1);
390                 int attrValue = arrayValue[index];
391                 sRMap.put(attrValue, Pair.of(ResourceType.ATTR, attrName));
392                 revRAttrMap.put(attrName, attrValue);
393             }
394             sRMap.put(index, Pair.of(ResourceType.STYLEABLE, name));
395             revRStyleableMap.put(name, index);
396         }
397     }
398 
399     @Override
dispose()400     public boolean dispose() {
401         BridgeAssetManager.clearSystem();
402 
403         // dispose of the default typeface.
404         Typeface_Delegate.resetDefaults();
405 
406         return true;
407     }
408 
409     /**
410      * Starts a layout session by inflating and rendering it. The method returns a
411      * {@link RenderSession} on which further actions can be taken.
412      * <p/>
413      * If {@link SessionParams} includes the {@link RenderParamsFlags#FLAG_DO_NOT_RENDER_ON_CREATE},
414      * this method will only inflate the layout but will NOT render it.
415      * @param params the {@link SessionParams} object with all the information necessary to create
416      *           the scene.
417      * @return a new {@link RenderSession} object that contains the result of the layout.
418      * @since 5
419      */
420     @Override
createSession(SessionParams params)421     public RenderSession createSession(SessionParams params) {
422         try {
423             Result lastResult = SUCCESS.createResult();
424             RenderSessionImpl scene = new RenderSessionImpl(params);
425             try {
426                 prepareThread();
427                 lastResult = scene.init(params.getTimeout());
428                 if (lastResult.isSuccess()) {
429                     lastResult = scene.inflate();
430 
431                     boolean doNotRenderOnCreate = Boolean.TRUE.equals(
432                             params.getFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE));
433                     if (lastResult.isSuccess() && !doNotRenderOnCreate) {
434                         lastResult = scene.render(true /*freshRender*/);
435                     }
436                 }
437             } finally {
438                 scene.release();
439                 cleanupThread();
440             }
441 
442             return new BridgeRenderSession(scene, lastResult);
443         } catch (Throwable t) {
444             // get the real cause of the exception.
445             Throwable t2 = t;
446             while (t2.getCause() != null) {
447                 t2 = t.getCause();
448             }
449             return new BridgeRenderSession(null,
450                     ERROR_UNKNOWN.createResult(t2.getMessage(), t));
451         }
452     }
453 
454     @Override
renderDrawable(DrawableParams params)455     public Result renderDrawable(DrawableParams params) {
456         try {
457             Result lastResult = SUCCESS.createResult();
458             RenderDrawable action = new RenderDrawable(params);
459             try {
460                 prepareThread();
461                 lastResult = action.init(params.getTimeout());
462                 if (lastResult.isSuccess()) {
463                     lastResult = action.render();
464                 }
465             } finally {
466                 action.release();
467                 cleanupThread();
468             }
469 
470             return lastResult;
471         } catch (Throwable t) {
472             // get the real cause of the exception.
473             Throwable t2 = t;
474             while (t2.getCause() != null) {
475                 t2 = t.getCause();
476             }
477             return ERROR_UNKNOWN.createResult(t2.getMessage(), t);
478         }
479     }
480 
481     @Override
clearCaches(Object projectKey)482     public void clearCaches(Object projectKey) {
483         if (projectKey != null) {
484             sProjectBitmapCache.remove(projectKey);
485             sProject9PatchCache.remove(projectKey);
486         }
487     }
488 
489     @Override
getViewParent(Object viewObject)490     public Result getViewParent(Object viewObject) {
491         if (viewObject instanceof View) {
492             return Status.SUCCESS.createResult(((View)viewObject).getParent());
493         }
494 
495         throw new IllegalArgumentException("viewObject is not a View");
496     }
497 
498     @Override
getViewIndex(Object viewObject)499     public Result getViewIndex(Object viewObject) {
500         if (viewObject instanceof View) {
501             View view = (View) viewObject;
502             ViewParent parentView = view.getParent();
503 
504             if (parentView instanceof ViewGroup) {
505                 Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view));
506             }
507 
508             return Status.SUCCESS.createResult();
509         }
510 
511         throw new IllegalArgumentException("viewObject is not a View");
512     }
513 
514     @Override
isRtl(String locale)515     public boolean isRtl(String locale) {
516         return isLocaleRtl(locale);
517     }
518 
isLocaleRtl(String locale)519     public static boolean isLocaleRtl(String locale) {
520         if (locale == null) {
521             locale = "";
522         }
523         ULocale uLocale = new ULocale(locale);
524         return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL);
525     }
526 
527     /**
528      * Returns the lock for the bridge
529      */
getLock()530     public static ReentrantLock getLock() {
531         return sLock;
532     }
533 
534     /**
535      * Prepares the current thread for rendering.
536      *
537      * Note that while this can be called several time, the first call to {@link #cleanupThread()}
538      * will do the clean-up, and make the thread unable to do further scene actions.
539      */
prepareThread()540     public synchronized static void prepareThread() {
541         // we need to make sure the Looper has been initialized for this thread.
542         // this is required for View that creates Handler objects.
543         if (Looper.myLooper() == null) {
544             Looper.prepareMainLooper();
545         }
546     }
547 
548     /**
549      * Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
550      * <p>
551      * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
552      * call to this will prevent the thread from doing further scene actions
553      */
cleanupThread()554     public synchronized static void cleanupThread() {
555         // clean up the looper
556         Looper_Accessor.cleanupThread();
557     }
558 
getLog()559     public static LayoutLog getLog() {
560         return sCurrentLog;
561     }
562 
setLog(LayoutLog log)563     public static void setLog(LayoutLog log) {
564         // check only the thread currently owning the lock can do this.
565         if (!sLock.isHeldByCurrentThread()) {
566             throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
567         }
568 
569         if (log != null) {
570             sCurrentLog = log;
571         } else {
572             sCurrentLog = sDefaultLog;
573         }
574     }
575 
576     /**
577      * Returns details of a framework resource from its integer value.
578      * @param value the integer value
579      * @return a Pair containing the resource type and name, or null if the id
580      *     does not match any resource.
581      */
resolveResourceId(int value)582     public static Pair<ResourceType, String> resolveResourceId(int value) {
583         Pair<ResourceType, String> pair = sRMap.get(value);
584         if (pair == null) {
585             pair = sDynamicIds.resolveId(value);
586             if (pair == null) {
587                 //System.out.println(String.format("Missing id: %1$08X (%1$d)", value));
588             }
589         }
590         return pair;
591     }
592 
593     /**
594      * Returns the name of a framework resource whose value is an int array.
595      */
resolveResourceId(int[] array)596     public static String resolveResourceId(int[] array) {
597         sIntArrayWrapper.set(array);
598         return sRArrayMap.get(sIntArrayWrapper);
599     }
600 
601     /**
602      * Returns the integer id of a framework resource, from a given resource type and resource name.
603      * <p/>
604      * If no resource is found, it creates a dynamic id for the resource.
605      *
606      * @param type the type of the resource
607      * @param name the name of the resource.
608      *
609      * @return an {@link Integer} containing the resource id.
610      */
611     @NonNull
getResourceId(ResourceType type, String name)612     public static Integer getResourceId(ResourceType type, String name) {
613         Map<String, Integer> map = sRevRMap.get(type);
614         Integer value = null;
615         if (map != null) {
616             value = map.get(name);
617         }
618 
619         return value == null ? sDynamicIds.getId(type, name) : value;
620 
621     }
622 
623     /**
624      * Returns the list of possible enums for a given attribute name.
625      */
getEnumValues(String attributeName)626     public static Map<String, Integer> getEnumValues(String attributeName) {
627         if (sEnumValueMap != null) {
628             return sEnumValueMap.get(attributeName);
629         }
630 
631         return null;
632     }
633 
634     /**
635      * Returns the platform build properties.
636      */
getPlatformProperties()637     public static Map<String, String> getPlatformProperties() {
638         return sPlatformProperties;
639     }
640 
641     /**
642      * Returns the bitmap for a specific path, from a specific project cache, or from the
643      * framework cache.
644      * @param value the path of the bitmap
645      * @param projectKey the key of the project, or null to query the framework cache.
646      * @return the cached Bitmap or null if not found.
647      */
getCachedBitmap(String value, Object projectKey)648     public static Bitmap getCachedBitmap(String value, Object projectKey) {
649         if (projectKey != null) {
650             Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
651             if (map != null) {
652                 SoftReference<Bitmap> ref = map.get(value);
653                 if (ref != null) {
654                     return ref.get();
655                 }
656             }
657         } else {
658             SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
659             if (ref != null) {
660                 return ref.get();
661             }
662         }
663 
664         return null;
665     }
666 
667     /**
668      * Sets a bitmap in a project cache or in the framework cache.
669      * @param value the path of the bitmap
670      * @param bmp the Bitmap object
671      * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
672      */
setCachedBitmap(String value, Bitmap bmp, Object projectKey)673     public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
674         if (projectKey != null) {
675             Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
676 
677             if (map == null) {
678                 map = new HashMap<String, SoftReference<Bitmap>>();
679                 sProjectBitmapCache.put(projectKey, map);
680             }
681 
682             map.put(value, new SoftReference<Bitmap>(bmp));
683         } else {
684             sFrameworkBitmapCache.put(value, new SoftReference<Bitmap>(bmp));
685         }
686     }
687 
688     /**
689      * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
690      * framework cache.
691      * @param value the path of the 9 patch
692      * @param projectKey the key of the project, or null to query the framework cache.
693      * @return the cached 9 patch or null if not found.
694      */
getCached9Patch(String value, Object projectKey)695     public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
696         if (projectKey != null) {
697             Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
698 
699             if (map != null) {
700                 SoftReference<NinePatchChunk> ref = map.get(value);
701                 if (ref != null) {
702                     return ref.get();
703                 }
704             }
705         } else {
706             SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
707             if (ref != null) {
708                 return ref.get();
709             }
710         }
711 
712         return null;
713     }
714 
715     /**
716      * Sets a 9 patch chunk in a project cache or in the framework cache.
717      * @param value the path of the 9 patch
718      * @param ninePatch the 9 patch object
719      * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
720      */
setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey)721     public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
722         if (projectKey != null) {
723             Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
724 
725             if (map == null) {
726                 map = new HashMap<String, SoftReference<NinePatchChunk>>();
727                 sProject9PatchCache.put(projectKey, map);
728             }
729 
730             map.put(value, new SoftReference<NinePatchChunk>(ninePatch));
731         } else {
732             sFramework9PatchCache.put(value, new SoftReference<NinePatchChunk>(ninePatch));
733         }
734     }
735 }
736