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