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