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