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