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