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