• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.concurrent.locks.ReentrantLock;
60 
61 import libcore.io.MemoryMappedFile_Delegate;
62 
63 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
64 
65 /**
66  * Main entry point of the LayoutLib Bridge.
67  * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call
68  * {@link #createSession(SessionParams)}
69  */
70 public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
71 
72     private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left";
73 
74     public static class StaticMethodNotImplementedException extends RuntimeException {
75         private static final long serialVersionUID = 1L;
76 
StaticMethodNotImplementedException(String msg)77         public StaticMethodNotImplementedException(String msg) {
78             super(msg);
79         }
80     }
81 
82     /**
83      * Lock to ensure only one rendering/inflating happens at a time.
84      * This is due to some singleton in the Android framework.
85      */
86     private final static ReentrantLock sLock = new ReentrantLock();
87 
88     /**
89      * Maps from id to resource type/name. This is for com.android.internal.R
90      */
91     @SuppressWarnings("deprecation")
92     private final static Map<Integer, Pair<ResourceType, String>> sRMap = new HashMap<>();
93 
94     /**
95      * Reverse map compared to sRMap, resource type -> (resource name -> id).
96      * This is for com.android.internal.R.
97      */
98     private final static Map<ResourceType, Map<String, Integer>> sRevRMap = new EnumMap<>(ResourceType.class);
99 
100     // framework resources are defined as 0x01XX#### where XX is the resource type (layout,
101     // drawable, etc...). Using FF as the type allows for 255 resource types before we get a
102     // collision which should be fine.
103     private final static int DYNAMIC_ID_SEED_START = 0x01ff0000;
104     private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START);
105 
106     private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache =
107             new HashMap<>();
108     private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache =
109 
110             new HashMap<>();
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 
359         return true;
360     }
361 
362     /**
363      * Starts a layout session by inflating and rendering it. The method returns a
364      * {@link RenderSession} on which further actions can be taken.
365      * <p/>
366      * If {@link SessionParams} includes the {@link RenderParamsFlags#FLAG_DO_NOT_RENDER_ON_CREATE},
367      * this method will only inflate the layout but will NOT render it.
368      * @param params the {@link SessionParams} object with all the information necessary to create
369      *           the scene.
370      * @return a new {@link RenderSession} object that contains the result of the layout.
371      * @since 5
372      */
373     @Override
createSession(SessionParams params)374     public RenderSession createSession(SessionParams params) {
375         try {
376             Result lastResult;
377             RenderSessionImpl scene = new RenderSessionImpl(params);
378             try {
379                 prepareThread();
380                 lastResult = scene.init(params.getTimeout());
381                 if (lastResult.isSuccess()) {
382                     lastResult = scene.inflate();
383 
384                     boolean doNotRenderOnCreate = Boolean.TRUE.equals(
385                             params.getFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE));
386                     if (lastResult.isSuccess() && !doNotRenderOnCreate) {
387                         lastResult = scene.render(true /*freshRender*/);
388                     }
389                 }
390             } finally {
391                 scene.release();
392                 cleanupThread();
393             }
394 
395             return new BridgeRenderSession(scene, lastResult);
396         } catch (Throwable t) {
397             // get the real cause of the exception.
398             Throwable t2 = t;
399             while (t2.getCause() != null) {
400                 t2 = t.getCause();
401             }
402             return new BridgeRenderSession(null,
403                     ERROR_UNKNOWN.createResult(t2.getMessage(), t));
404         }
405     }
406 
407     @Override
renderDrawable(DrawableParams params)408     public Result renderDrawable(DrawableParams params) {
409         try {
410             Result lastResult;
411             RenderDrawable action = new RenderDrawable(params);
412             try {
413                 prepareThread();
414                 lastResult = action.init(params.getTimeout());
415                 if (lastResult.isSuccess()) {
416                     lastResult = action.render();
417                 }
418             } finally {
419                 action.release();
420                 cleanupThread();
421             }
422 
423             return lastResult;
424         } catch (Throwable t) {
425             // get the real cause of the exception.
426             Throwable t2 = t;
427             while (t2.getCause() != null) {
428                 t2 = t.getCause();
429             }
430             return ERROR_UNKNOWN.createResult(t2.getMessage(), t);
431         }
432     }
433 
434     @Override
clearCaches(Object projectKey)435     public void clearCaches(Object projectKey) {
436         if (projectKey != null) {
437             sProjectBitmapCache.remove(projectKey);
438             sProject9PatchCache.remove(projectKey);
439         }
440     }
441 
442     @Override
getViewParent(Object viewObject)443     public Result getViewParent(Object viewObject) {
444         if (viewObject instanceof View) {
445             return Status.SUCCESS.createResult(((View)viewObject).getParent());
446         }
447 
448         throw new IllegalArgumentException("viewObject is not a View");
449     }
450 
451     @Override
getViewIndex(Object viewObject)452     public Result getViewIndex(Object viewObject) {
453         if (viewObject instanceof View) {
454             View view = (View) viewObject;
455             ViewParent parentView = view.getParent();
456 
457             if (parentView instanceof ViewGroup) {
458                 Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view));
459             }
460 
461             return Status.SUCCESS.createResult();
462         }
463 
464         throw new IllegalArgumentException("viewObject is not a View");
465     }
466 
467     @Override
isRtl(String locale)468     public boolean isRtl(String locale) {
469         return isLocaleRtl(locale);
470     }
471 
isLocaleRtl(String locale)472     public static boolean isLocaleRtl(String locale) {
473         if (locale == null) {
474             locale = "";
475         }
476         ULocale uLocale = new ULocale(locale);
477         return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL);
478     }
479 
480     /**
481      * Returns the lock for the bridge
482      */
getLock()483     public static ReentrantLock getLock() {
484         return sLock;
485     }
486 
487     /**
488      * Prepares the current thread for rendering.
489      *
490      * Note that while this can be called several time, the first call to {@link #cleanupThread()}
491      * will do the clean-up, and make the thread unable to do further scene actions.
492      */
prepareThread()493     public synchronized static void prepareThread() {
494         // we need to make sure the Looper has been initialized for this thread.
495         // this is required for View that creates Handler objects.
496         if (Looper.myLooper() == null) {
497             Looper.prepareMainLooper();
498         }
499     }
500 
501     /**
502      * Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
503      * <p>
504      * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
505      * call to this will prevent the thread from doing further scene actions
506      */
cleanupThread()507     public synchronized static void cleanupThread() {
508         // clean up the looper
509         Looper_Accessor.cleanupThread();
510     }
511 
getLog()512     public static LayoutLog getLog() {
513         return sCurrentLog;
514     }
515 
setLog(LayoutLog log)516     public static void setLog(LayoutLog log) {
517         // check only the thread currently owning the lock can do this.
518         if (!sLock.isHeldByCurrentThread()) {
519             throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
520         }
521 
522         if (log != null) {
523             sCurrentLog = log;
524         } else {
525             sCurrentLog = sDefaultLog;
526         }
527     }
528 
529     /**
530      * Returns details of a framework resource from its integer value.
531      * @param value the integer value
532      * @return a Pair containing the resource type and name, or null if the id
533      *     does not match any resource.
534      */
535     @SuppressWarnings("deprecation")
resolveResourceId(int value)536     public static Pair<ResourceType, String> resolveResourceId(int value) {
537         Pair<ResourceType, String> pair = sRMap.get(value);
538         if (pair == null) {
539             pair = sDynamicIds.resolveId(value);
540         }
541         return pair;
542     }
543 
544     /**
545      * Returns the integer id of a framework resource, from a given resource type and resource name.
546      * <p/>
547      * If no resource is found, it creates a dynamic id for the resource.
548      *
549      * @param type the type of the resource
550      * @param name the name of the resource.
551      *
552      * @return an {@link Integer} containing the resource id.
553      */
554     @NonNull
getResourceId(ResourceType type, String name)555     public static Integer getResourceId(ResourceType type, String name) {
556         Map<String, Integer> map = sRevRMap.get(type);
557         Integer value = null;
558         if (map != null) {
559             value = map.get(name);
560         }
561 
562         return value == null ? sDynamicIds.getId(type, name) : value;
563 
564     }
565 
566     /**
567      * Returns the list of possible enums for a given attribute name.
568      */
getEnumValues(String attributeName)569     public static Map<String, Integer> getEnumValues(String attributeName) {
570         if (sEnumValueMap != null) {
571             return sEnumValueMap.get(attributeName);
572         }
573 
574         return null;
575     }
576 
577     /**
578      * Returns the platform build properties.
579      */
getPlatformProperties()580     public static Map<String, String> getPlatformProperties() {
581         return sPlatformProperties;
582     }
583 
584     /**
585      * Returns the bitmap for a specific path, from a specific project cache, or from the
586      * framework cache.
587      * @param value the path of the bitmap
588      * @param projectKey the key of the project, or null to query the framework cache.
589      * @return the cached Bitmap or null if not found.
590      */
getCachedBitmap(String value, Object projectKey)591     public static Bitmap getCachedBitmap(String value, Object projectKey) {
592         if (projectKey != null) {
593             Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
594             if (map != null) {
595                 SoftReference<Bitmap> ref = map.get(value);
596                 if (ref != null) {
597                     return ref.get();
598                 }
599             }
600         } else {
601             SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
602             if (ref != null) {
603                 return ref.get();
604             }
605         }
606 
607         return null;
608     }
609 
610     /**
611      * Sets a bitmap in a project cache or in the framework cache.
612      * @param value the path of the bitmap
613      * @param bmp the Bitmap object
614      * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
615      */
setCachedBitmap(String value, Bitmap bmp, Object projectKey)616     public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
617         if (projectKey != null) {
618             Map<String, SoftReference<Bitmap>> map =
619                     sProjectBitmapCache.computeIfAbsent(projectKey, k -> new HashMap<>());
620 
621             map.put(value, new SoftReference<>(bmp));
622         } else {
623             sFrameworkBitmapCache.put(value, new SoftReference<>(bmp));
624         }
625     }
626 
627     /**
628      * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
629      * framework cache.
630      * @param value the path of the 9 patch
631      * @param projectKey the key of the project, or null to query the framework cache.
632      * @return the cached 9 patch or null if not found.
633      */
getCached9Patch(String value, Object projectKey)634     public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
635         if (projectKey != null) {
636             Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
637 
638             if (map != null) {
639                 SoftReference<NinePatchChunk> ref = map.get(value);
640                 if (ref != null) {
641                     return ref.get();
642                 }
643             }
644         } else {
645             SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
646             if (ref != null) {
647                 return ref.get();
648             }
649         }
650 
651         return null;
652     }
653 
654     /**
655      * Sets a 9 patch chunk in a project cache or in the framework cache.
656      * @param value the path of the 9 patch
657      * @param ninePatch the 9 patch object
658      * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
659      */
setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey)660     public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
661         if (projectKey != null) {
662             Map<String, SoftReference<NinePatchChunk>> map =
663                     sProject9PatchCache.computeIfAbsent(projectKey, k -> new HashMap<>());
664 
665             map.put(value, new SoftReference<>(ninePatch));
666         } else {
667             sFramework9PatchCache.put(value, new SoftReference<>(ninePatch));
668         }
669     }
670 }
671