1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge;
18
19import com.android.ide.common.rendering.api.Capability;
20import com.android.ide.common.rendering.api.DrawableParams;
21import com.android.ide.common.rendering.api.Features;
22import com.android.ide.common.rendering.api.LayoutLog;
23import com.android.ide.common.rendering.api.RenderSession;
24import com.android.ide.common.rendering.api.Result;
25import com.android.ide.common.rendering.api.Result.Status;
26import com.android.ide.common.rendering.api.SessionParams;
27import com.android.layoutlib.bridge.android.RenderParamsFlags;
28import com.android.layoutlib.bridge.impl.RenderDrawable;
29import com.android.layoutlib.bridge.impl.RenderSessionImpl;
30import com.android.layoutlib.bridge.util.DynamicIdMap;
31import com.android.ninepatch.NinePatchChunk;
32import com.android.resources.ResourceType;
33import com.android.tools.layoutlib.create.MethodAdapter;
34import com.android.tools.layoutlib.create.OverrideMethod;
35import com.android.util.Pair;
36
37import android.annotation.NonNull;
38import android.content.res.BridgeAssetManager;
39import android.graphics.Bitmap;
40import android.graphics.FontFamily_Delegate;
41import android.graphics.Typeface_Delegate;
42import android.icu.util.ULocale;
43import android.os.Looper;
44import android.os.Looper_Accessor;
45import android.view.View;
46import android.view.ViewGroup;
47import android.view.ViewParent;
48
49import java.io.File;
50import java.lang.ref.SoftReference;
51import java.lang.reflect.Field;
52import java.lang.reflect.Modifier;
53import java.util.Arrays;
54import java.util.Comparator;
55import java.util.EnumMap;
56import java.util.EnumSet;
57import java.util.HashMap;
58import java.util.Map;
59import java.util.concurrent.locks.ReentrantLock;
60
61import libcore.io.MemoryMappedFile_Delegate;
62
63import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
64import 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 */
71public 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
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
131        private IntArray() {
132            // do nothing
133        }
134
135        private IntArray(int[] a) {
136            mArray = a;
137        }
138
139        private void set(int[] a) {
140            mArray = a;
141        }
142
143        @Override
144        public int hashCode() {
145            return Arrays.hashCode(mArray);
146        }
147
148        @Override
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
190    public int getApiLevel() {
191        return com.android.ide.common.rendering.api.Bridge.API_CURRENT;
192    }
193
194    @Override
195    @Deprecated
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
203    public boolean supports(int feature) {
204        return feature <= LAST_SUPPORTED_FEATURE;
205    }
206
207    @Override
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     */
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
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
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
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
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
482    public void clearCaches(Object projectKey) {
483        if (projectKey != null) {
484            sProjectBitmapCache.remove(projectKey);
485            sProject9PatchCache.remove(projectKey);
486        }
487    }
488
489    @Override
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
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
515    public boolean isRtl(String locale) {
516        return isLocaleRtl(locale);
517    }
518
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     */
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     */
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     */
554    public synchronized static void cleanupThread() {
555        // clean up the looper
556        Looper_Accessor.cleanupThread();
557    }
558
559    public static LayoutLog getLog() {
560        return sCurrentLog;
561    }
562
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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