Bridge.java revision b1484862e2367d87d3ccbd0fd0a6d2598ed5918a
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.impl.RenderDrawable;
28import com.android.layoutlib.bridge.impl.RenderSessionImpl;
29import com.android.layoutlib.bridge.util.DynamicIdMap;
30import com.android.ninepatch.NinePatchChunk;
31import com.android.resources.ResourceType;
32import com.android.tools.layoutlib.create.MethodAdapter;
33import com.android.tools.layoutlib.create.OverrideMethod;
34import com.android.util.Pair;
35
36import android.annotation.NonNull;
37import android.content.res.BridgeAssetManager;
38import android.graphics.Bitmap;
39import android.graphics.FontFamily_Delegate;
40import android.graphics.Typeface_Delegate;
41import android.icu.util.ULocale;
42import android.os.Looper;
43import android.os.Looper_Accessor;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.ViewParent;
47
48import java.io.File;
49import java.lang.ref.SoftReference;
50import java.lang.reflect.Field;
51import java.lang.reflect.Modifier;
52import java.util.Arrays;
53import java.util.Comparator;
54import java.util.EnumMap;
55import java.util.EnumSet;
56import java.util.HashMap;
57import java.util.Map;
58import java.util.concurrent.locks.ReentrantLock;
59
60import libcore.io.MemoryMappedFile_Delegate;
61
62import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
63import 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 */
70public 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
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
130        private IntArray() {
131            // do nothing
132        }
133
134        private IntArray(int[] a) {
135            mArray = a;
136        }
137
138        private void set(int[] a) {
139            mArray = a;
140        }
141
142        @Override
143        public int hashCode() {
144            return Arrays.hashCode(mArray);
145        }
146
147        @Override
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.RENDER_ALL_DRAWABLE_STATES;
187
188    @Override
189    public int getApiLevel() {
190        return com.android.ide.common.rendering.api.Bridge.API_CURRENT;
191    }
192
193    @Override
194    @Deprecated
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
202    public boolean supports(int feature) {
203        return feature <= LAST_SUPPORTED_FEATURE;
204    }
205
206    @Override
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     */
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
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
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
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
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
476    public void clearCaches(Object projectKey) {
477        if (projectKey != null) {
478            sProjectBitmapCache.remove(projectKey);
479            sProject9PatchCache.remove(projectKey);
480        }
481    }
482
483    @Override
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
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
509    public boolean isRtl(String locale) {
510        return isLocaleRtl(locale);
511    }
512
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     */
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     */
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     */
548    public static void cleanupThread() {
549        // clean up the looper
550        Looper_Accessor.cleanupThread();
551    }
552
553    public static LayoutLog getLog() {
554        return sCurrentLog;
555    }
556
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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