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