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