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