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