1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package android.content.res; 17 18 import org.xmlpull.v1.XmlPullParser; 19 import org.xmlpull.v1.XmlPullParserException; 20 21 import android.animation.Animator; 22 import android.animation.StateListAnimator; 23 import android.annotation.AnyRes; 24 import android.annotation.AttrRes; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.annotation.PluralsRes; 28 import android.annotation.RawRes; 29 import android.annotation.StyleRes; 30 import android.annotation.StyleableRes; 31 import android.content.pm.ActivityInfo; 32 import android.content.pm.ActivityInfo.Config; 33 import android.content.res.Resources.NotFoundException; 34 import android.graphics.drawable.ColorDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.icu.text.PluralRules; 37 import android.os.Build; 38 import android.os.LocaleList; 39 import android.os.Trace; 40 import android.util.AttributeSet; 41 import android.util.DisplayMetrics; 42 import android.util.Log; 43 import android.util.LongSparseArray; 44 import android.util.Slog; 45 import android.util.TypedValue; 46 import android.util.Xml; 47 import android.view.Display; 48 import android.view.DisplayAdjustments; 49 50 import java.io.InputStream; 51 import java.util.Arrays; 52 import java.util.Locale; 53 54 /** 55 * The implementation of Resource access. This class contains the AssetManager and all caches 56 * associated with it. 57 * 58 * {@link Resources} is just a thing wrapper around this class. When a configuration change 59 * occurs, clients can retain the same {@link Resources} reference because the underlying 60 * {@link ResourcesImpl} object will be updated or re-created. 61 * 62 * @hide 63 */ 64 public class ResourcesImpl { 65 static final String TAG = "Resources"; 66 67 private static final boolean DEBUG_LOAD = false; 68 private static final boolean DEBUG_CONFIG = false; 69 private static final boolean TRACE_FOR_PRELOAD = false; 70 private static final boolean TRACE_FOR_MISS_PRELOAD = false; 71 72 private static final int LAYOUT_DIR_CONFIG = ActivityInfo.activityInfoConfigJavaToNative( 73 ActivityInfo.CONFIG_LAYOUT_DIRECTION); 74 75 private static final int ID_OTHER = 0x01000004; 76 77 private static final Object sSync = new Object(); 78 79 private static boolean sPreloaded; 80 private boolean mPreloading; 81 82 // Information about preloaded resources. Note that they are not 83 // protected by a lock, because while preloading in zygote we are all 84 // single-threaded, and after that these are immutable. 85 private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables; 86 private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables 87 = new LongSparseArray<>(); 88 private static final LongSparseArray<android.content.res.ConstantState<ComplexColor>> 89 sPreloadedComplexColors = new LongSparseArray<>(); 90 91 /** Lock object used to protect access to caches and configuration. */ 92 private final Object mAccessLock = new Object(); 93 94 // These are protected by mAccessLock. 95 private final Configuration mTmpConfig = new Configuration(); 96 private final DrawableCache mDrawableCache = new DrawableCache(); 97 private final DrawableCache mColorDrawableCache = new DrawableCache(); 98 private final ConfigurationBoundResourceCache<ComplexColor> mComplexColorCache = 99 new ConfigurationBoundResourceCache<>(); 100 private final ConfigurationBoundResourceCache<Animator> mAnimatorCache = 101 new ConfigurationBoundResourceCache<>(); 102 private final ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache = 103 new ConfigurationBoundResourceCache<>(); 104 105 /** Size of the cyclical cache used to map XML files to blocks. */ 106 private static final int XML_BLOCK_CACHE_SIZE = 4; 107 108 // Cyclical cache used for recently-accessed XML files. 109 private int mLastCachedXmlBlockIndex = -1; 110 private final int[] mCachedXmlBlockCookies = new int[XML_BLOCK_CACHE_SIZE]; 111 private final String[] mCachedXmlBlockFiles = new String[XML_BLOCK_CACHE_SIZE]; 112 private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[XML_BLOCK_CACHE_SIZE]; 113 114 115 final AssetManager mAssets; 116 private final DisplayMetrics mMetrics = new DisplayMetrics(); 117 private final DisplayAdjustments mDisplayAdjustments; 118 119 private PluralRules mPluralRule; 120 121 private final Configuration mConfiguration = new Configuration(); 122 123 static { 124 sPreloadedDrawables = new LongSparseArray[2]; 125 sPreloadedDrawables[0] = new LongSparseArray<>(); 126 sPreloadedDrawables[1] = new LongSparseArray<>(); 127 } 128 129 /** 130 * Creates a new ResourcesImpl object with CompatibilityInfo. 131 * 132 * @param assets Previously created AssetManager. 133 * @param metrics Current display metrics to consider when 134 * selecting/computing resource values. 135 * @param config Desired device configuration to consider when 136 * selecting/computing resource values (optional). 137 * @param displayAdjustments this resource's Display override and compatibility info. 138 * Must not be null. 139 */ ResourcesImpl(@onNull AssetManager assets, @Nullable DisplayMetrics metrics, @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments)140 public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics, 141 @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) { 142 mAssets = assets; 143 mMetrics.setToDefaults(); 144 mDisplayAdjustments = displayAdjustments; 145 updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo()); 146 mAssets.ensureStringBlocks(); 147 } 148 getDisplayAdjustments()149 public DisplayAdjustments getDisplayAdjustments() { 150 return mDisplayAdjustments; 151 } 152 getAssets()153 public AssetManager getAssets() { 154 return mAssets; 155 } 156 getDisplayMetrics()157 DisplayMetrics getDisplayMetrics() { 158 if (DEBUG_CONFIG) Slog.v(TAG, "Returning DisplayMetrics: " + mMetrics.widthPixels 159 + "x" + mMetrics.heightPixels + " " + mMetrics.density); 160 return mMetrics; 161 } 162 getConfiguration()163 Configuration getConfiguration() { 164 return mConfiguration; 165 } 166 getSizeConfigurations()167 Configuration[] getSizeConfigurations() { 168 return mAssets.getSizeConfigurations(); 169 } 170 getCompatibilityInfo()171 CompatibilityInfo getCompatibilityInfo() { 172 return mDisplayAdjustments.getCompatibilityInfo(); 173 } 174 getPluralRule()175 private PluralRules getPluralRule() { 176 synchronized (sSync) { 177 if (mPluralRule == null) { 178 mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().get(0)); 179 } 180 return mPluralRule; 181 } 182 } 183 getValue(@nyRes int id, TypedValue outValue, boolean resolveRefs)184 void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) 185 throws NotFoundException { 186 boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs); 187 if (found) { 188 return; 189 } 190 throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)); 191 } 192 getValueForDensity(@nyRes int id, int density, TypedValue outValue, boolean resolveRefs)193 void getValueForDensity(@AnyRes int id, int density, TypedValue outValue, 194 boolean resolveRefs) throws NotFoundException { 195 boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs); 196 if (found) { 197 return; 198 } 199 throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)); 200 } 201 getValue(String name, TypedValue outValue, boolean resolveRefs)202 void getValue(String name, TypedValue outValue, boolean resolveRefs) 203 throws NotFoundException { 204 int id = getIdentifier(name, "string", null); 205 if (id != 0) { 206 getValue(id, outValue, resolveRefs); 207 return; 208 } 209 throw new NotFoundException("String resource name " + name); 210 } 211 getIdentifier(String name, String defType, String defPackage)212 int getIdentifier(String name, String defType, String defPackage) { 213 if (name == null) { 214 throw new NullPointerException("name is null"); 215 } 216 try { 217 return Integer.parseInt(name); 218 } catch (Exception e) { 219 // Ignore 220 } 221 return mAssets.getResourceIdentifier(name, defType, defPackage); 222 } 223 224 @NonNull getResourceName(@nyRes int resid)225 String getResourceName(@AnyRes int resid) throws NotFoundException { 226 String str = mAssets.getResourceName(resid); 227 if (str != null) return str; 228 throw new NotFoundException("Unable to find resource ID #0x" 229 + Integer.toHexString(resid)); 230 } 231 232 @NonNull getResourcePackageName(@nyRes int resid)233 String getResourcePackageName(@AnyRes int resid) throws NotFoundException { 234 String str = mAssets.getResourcePackageName(resid); 235 if (str != null) return str; 236 throw new NotFoundException("Unable to find resource ID #0x" 237 + Integer.toHexString(resid)); 238 } 239 240 @NonNull getResourceTypeName(@nyRes int resid)241 String getResourceTypeName(@AnyRes int resid) throws NotFoundException { 242 String str = mAssets.getResourceTypeName(resid); 243 if (str != null) return str; 244 throw new NotFoundException("Unable to find resource ID #0x" 245 + Integer.toHexString(resid)); 246 } 247 248 @NonNull getResourceEntryName(@nyRes int resid)249 String getResourceEntryName(@AnyRes int resid) throws NotFoundException { 250 String str = mAssets.getResourceEntryName(resid); 251 if (str != null) return str; 252 throw new NotFoundException("Unable to find resource ID #0x" 253 + Integer.toHexString(resid)); 254 } 255 256 @NonNull getQuantityText(@luralsRes int id, int quantity)257 CharSequence getQuantityText(@PluralsRes int id, int quantity) throws NotFoundException { 258 PluralRules rule = getPluralRule(); 259 CharSequence res = mAssets.getResourceBagText(id, 260 attrForQuantityCode(rule.select(quantity))); 261 if (res != null) { 262 return res; 263 } 264 res = mAssets.getResourceBagText(id, ID_OTHER); 265 if (res != null) { 266 return res; 267 } 268 throw new NotFoundException("Plural resource ID #0x" + Integer.toHexString(id) 269 + " quantity=" + quantity 270 + " item=" + rule.select(quantity)); 271 } 272 attrForQuantityCode(String quantityCode)273 private static int attrForQuantityCode(String quantityCode) { 274 switch (quantityCode) { 275 case PluralRules.KEYWORD_ZERO: return 0x01000005; 276 case PluralRules.KEYWORD_ONE: return 0x01000006; 277 case PluralRules.KEYWORD_TWO: return 0x01000007; 278 case PluralRules.KEYWORD_FEW: return 0x01000008; 279 case PluralRules.KEYWORD_MANY: return 0x01000009; 280 default: return ID_OTHER; 281 } 282 } 283 284 @NonNull openRawResourceFd(@awRes int id, TypedValue tempValue)285 AssetFileDescriptor openRawResourceFd(@RawRes int id, TypedValue tempValue) 286 throws NotFoundException { 287 getValue(id, tempValue, true); 288 try { 289 return mAssets.openNonAssetFd(tempValue.assetCookie, tempValue.string.toString()); 290 } catch (Exception e) { 291 throw new NotFoundException("File " + tempValue.string.toString() + " from drawable " 292 + "resource ID #0x" + Integer.toHexString(id), e); 293 } 294 } 295 296 @NonNull openRawResource(@awRes int id, TypedValue value)297 InputStream openRawResource(@RawRes int id, TypedValue value) throws NotFoundException { 298 getValue(id, value, true); 299 try { 300 return mAssets.openNonAsset(value.assetCookie, value.string.toString(), 301 AssetManager.ACCESS_STREAMING); 302 } catch (Exception e) { 303 // Note: value.string might be null 304 NotFoundException rnf = new NotFoundException("File " 305 + (value.string == null ? "(null)" : value.string.toString()) 306 + " from drawable resource ID #0x" + Integer.toHexString(id)); 307 rnf.initCause(e); 308 throw rnf; 309 } 310 } 311 getAnimatorCache()312 ConfigurationBoundResourceCache<Animator> getAnimatorCache() { 313 return mAnimatorCache; 314 } 315 getStateListAnimatorCache()316 ConfigurationBoundResourceCache<StateListAnimator> getStateListAnimatorCache() { 317 return mStateListAnimatorCache; 318 } 319 updateConfiguration(Configuration config, DisplayMetrics metrics, CompatibilityInfo compat)320 public void updateConfiguration(Configuration config, DisplayMetrics metrics, 321 CompatibilityInfo compat) { 322 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration"); 323 try { 324 synchronized (mAccessLock) { 325 if (false) { 326 Slog.i(TAG, "**** Updating config of " + this + ": old config is " 327 + mConfiguration + " old compat is " 328 + mDisplayAdjustments.getCompatibilityInfo()); 329 Slog.i(TAG, "**** Updating config of " + this + ": new config is " 330 + config + " new compat is " + compat); 331 } 332 if (compat != null) { 333 mDisplayAdjustments.setCompatibilityInfo(compat); 334 } 335 if (metrics != null) { 336 mMetrics.setTo(metrics); 337 } 338 // NOTE: We should re-arrange this code to create a Display 339 // with the CompatibilityInfo that is used everywhere we deal 340 // with the display in relation to this app, rather than 341 // doing the conversion here. This impl should be okay because 342 // we make sure to return a compatible display in the places 343 // where there are public APIs to retrieve the display... but 344 // it would be cleaner and more maintainable to just be 345 // consistently dealing with a compatible display everywhere in 346 // the framework. 347 mDisplayAdjustments.getCompatibilityInfo().applyToDisplayMetrics(mMetrics); 348 349 final @Config int configChanges = calcConfigChanges(config); 350 351 // If even after the update there are no Locales set, grab the default locales. 352 LocaleList locales = mConfiguration.getLocales(); 353 if (locales.isEmpty()) { 354 locales = LocaleList.getDefault(); 355 mConfiguration.setLocales(locales); 356 } 357 358 if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) { 359 if (locales.size() > 1) { 360 // The LocaleList has changed. We must query the AssetManager's available 361 // Locales and figure out the best matching Locale in the new LocaleList. 362 String[] availableLocales = mAssets.getNonSystemLocales(); 363 if (LocaleList.isPseudoLocalesOnly(availableLocales)) { 364 // No app defined locales, so grab the system locales. 365 availableLocales = mAssets.getLocales(); 366 if (LocaleList.isPseudoLocalesOnly(availableLocales)) { 367 availableLocales = null; 368 } 369 } 370 371 if (availableLocales != null) { 372 final Locale bestLocale = locales.getFirstMatchWithEnglishSupported( 373 availableLocales); 374 if (bestLocale != null && bestLocale != locales.get(0)) { 375 mConfiguration.setLocales(new LocaleList(bestLocale, locales)); 376 } 377 } 378 } 379 } 380 381 if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) { 382 mMetrics.densityDpi = mConfiguration.densityDpi; 383 mMetrics.density = 384 mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; 385 } 386 mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale; 387 388 final int width, height; 389 if (mMetrics.widthPixels >= mMetrics.heightPixels) { 390 width = mMetrics.widthPixels; 391 height = mMetrics.heightPixels; 392 } else { 393 //noinspection SuspiciousNameCombination 394 width = mMetrics.heightPixels; 395 //noinspection SuspiciousNameCombination 396 height = mMetrics.widthPixels; 397 } 398 399 final int keyboardHidden; 400 if (mConfiguration.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO 401 && mConfiguration.hardKeyboardHidden 402 == Configuration.HARDKEYBOARDHIDDEN_YES) { 403 keyboardHidden = Configuration.KEYBOARDHIDDEN_SOFT; 404 } else { 405 keyboardHidden = mConfiguration.keyboardHidden; 406 } 407 408 mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc, 409 adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()), 410 mConfiguration.orientation, 411 mConfiguration.touchscreen, 412 mConfiguration.densityDpi, mConfiguration.keyboard, 413 keyboardHidden, mConfiguration.navigation, width, height, 414 mConfiguration.smallestScreenWidthDp, 415 mConfiguration.screenWidthDp, mConfiguration.screenHeightDp, 416 mConfiguration.screenLayout, mConfiguration.uiMode, 417 Build.VERSION.RESOURCES_SDK_INT); 418 419 if (DEBUG_CONFIG) { 420 Slog.i(TAG, "**** Updating config of " + this + ": final config is " 421 + mConfiguration + " final compat is " 422 + mDisplayAdjustments.getCompatibilityInfo()); 423 } 424 425 mDrawableCache.onConfigurationChange(configChanges); 426 mColorDrawableCache.onConfigurationChange(configChanges); 427 mComplexColorCache.onConfigurationChange(configChanges); 428 mAnimatorCache.onConfigurationChange(configChanges); 429 mStateListAnimatorCache.onConfigurationChange(configChanges); 430 431 flushLayoutCache(); 432 } 433 synchronized (sSync) { 434 if (mPluralRule != null) { 435 mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().get(0)); 436 } 437 } 438 } finally { 439 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 440 } 441 } 442 443 /** 444 * Applies the new configuration, returning a bitmask of the changes 445 * between the old and new configurations. 446 * 447 * @param config the new configuration 448 * @return bitmask of config changes 449 */ calcConfigChanges(@ullable Configuration config)450 public @Config int calcConfigChanges(@Nullable Configuration config) { 451 if (config == null) { 452 // If there is no configuration, assume all flags have changed. 453 return 0xFFFFFFFF; 454 } 455 456 mTmpConfig.setTo(config); 457 int density = config.densityDpi; 458 if (density == Configuration.DENSITY_DPI_UNDEFINED) { 459 density = mMetrics.noncompatDensityDpi; 460 } 461 462 mDisplayAdjustments.getCompatibilityInfo().applyToConfiguration(density, mTmpConfig); 463 464 if (mTmpConfig.getLocales().isEmpty()) { 465 mTmpConfig.setLocales(LocaleList.getDefault()); 466 } 467 return mConfiguration.updateFrom(mTmpConfig); 468 } 469 470 /** 471 * {@code Locale.toLanguageTag} will transform the obsolete (and deprecated) 472 * language codes "in", "ji" and "iw" to "id", "yi" and "he" respectively. 473 * 474 * All released versions of android prior to "L" used the deprecated language 475 * tags, so we will need to support them for backwards compatibility. 476 * 477 * Note that this conversion needs to take place *after* the call to 478 * {@code toLanguageTag} because that will convert all the deprecated codes to 479 * the new ones, even if they're set manually. 480 */ adjustLanguageTag(String languageTag)481 private static String adjustLanguageTag(String languageTag) { 482 final int separator = languageTag.indexOf('-'); 483 final String language; 484 final String remainder; 485 486 if (separator == -1) { 487 language = languageTag; 488 remainder = ""; 489 } else { 490 language = languageTag.substring(0, separator); 491 remainder = languageTag.substring(separator); 492 } 493 494 return Locale.adjustLanguageCode(language) + remainder; 495 } 496 497 /** 498 * Call this to remove all cached loaded layout resources from the 499 * Resources object. Only intended for use with performance testing 500 * tools. 501 */ flushLayoutCache()502 public void flushLayoutCache() { 503 synchronized (mCachedXmlBlocks) { 504 Arrays.fill(mCachedXmlBlockCookies, 0); 505 Arrays.fill(mCachedXmlBlockFiles, null); 506 507 final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks; 508 for (int i = 0; i < XML_BLOCK_CACHE_SIZE; i++) { 509 final XmlBlock oldBlock = cachedXmlBlocks[i]; 510 if (oldBlock != null) { 511 oldBlock.close(); 512 } 513 } 514 Arrays.fill(cachedXmlBlocks, null); 515 } 516 } 517 518 @Nullable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache)519 Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, 520 boolean useCache) throws NotFoundException { 521 try { 522 if (TRACE_FOR_PRELOAD) { 523 // Log only framework resources 524 if ((id >>> 24) == 0x1) { 525 final String name = getResourceName(id); 526 if (name != null) { 527 Log.d("PreloadDrawable", name); 528 } 529 } 530 } 531 532 final boolean isColorDrawable; 533 final DrawableCache caches; 534 final long key; 535 if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT 536 && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { 537 isColorDrawable = true; 538 caches = mColorDrawableCache; 539 key = value.data; 540 } else { 541 isColorDrawable = false; 542 caches = mDrawableCache; 543 key = (((long) value.assetCookie) << 32) | value.data; 544 } 545 546 // First, check whether we have a cached version of this drawable 547 // that was inflated against the specified theme. Skip the cache if 548 // we're currently preloading or we're not using the cache. 549 if (!mPreloading && useCache) { 550 final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme); 551 if (cachedDrawable != null) { 552 return cachedDrawable; 553 } 554 } 555 556 // Next, check preloaded drawables. Preloaded drawables may contain 557 // unresolved theme attributes. 558 final Drawable.ConstantState cs; 559 if (isColorDrawable) { 560 cs = sPreloadedColorDrawables.get(key); 561 } else { 562 cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); 563 } 564 565 Drawable dr; 566 if (cs != null) { 567 dr = cs.newDrawable(wrapper); 568 } else if (isColorDrawable) { 569 dr = new ColorDrawable(value.data); 570 } else { 571 dr = loadDrawableForCookie(wrapper, value, id, null); 572 } 573 574 // Determine if the drawable has unresolved theme attributes. If it 575 // does, we'll need to apply a theme and store it in a theme-specific 576 // cache. 577 final boolean canApplyTheme = dr != null && dr.canApplyTheme(); 578 if (canApplyTheme && theme != null) { 579 dr = dr.mutate(); 580 dr.applyTheme(theme); 581 dr.clearMutated(); 582 } 583 584 // If we were able to obtain a drawable, store it in the appropriate 585 // cache: preload, not themed, null theme, or theme-specific. Don't 586 // pollute the cache with drawables loaded from a foreign density. 587 if (dr != null && useCache) { 588 dr.setChangingConfigurations(value.changingConfigurations); 589 cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr); 590 } 591 592 return dr; 593 } catch (Exception e) { 594 String name; 595 try { 596 name = getResourceName(id); 597 } catch (NotFoundException e2) { 598 name = "(missing name)"; 599 } 600 601 // The target drawable might fail to load for any number of 602 // reasons, but we always want to include the resource name. 603 // Since the client already expects this method to throw a 604 // NotFoundException, just throw one of those. 605 final NotFoundException nfe = new NotFoundException("Drawable " + name 606 + " with resource ID #0x" + Integer.toHexString(id), e); 607 nfe.setStackTrace(new StackTraceElement[0]); 608 throw nfe; 609 } 610 } 611 cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches, Resources.Theme theme, boolean usesTheme, long key, Drawable dr)612 private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches, 613 Resources.Theme theme, boolean usesTheme, long key, Drawable dr) { 614 final Drawable.ConstantState cs = dr.getConstantState(); 615 if (cs == null) { 616 return; 617 } 618 619 if (mPreloading) { 620 final int changingConfigs = cs.getChangingConfigurations(); 621 if (isColorDrawable) { 622 if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) { 623 sPreloadedColorDrawables.put(key, cs); 624 } 625 } else { 626 if (verifyPreloadConfig( 627 changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) { 628 if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) { 629 // If this resource does not vary based on layout direction, 630 // we can put it in all of the preload maps. 631 sPreloadedDrawables[0].put(key, cs); 632 sPreloadedDrawables[1].put(key, cs); 633 } else { 634 // Otherwise, only in the layout dir we loaded it for. 635 sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs); 636 } 637 } 638 } 639 } else { 640 synchronized (mAccessLock) { 641 caches.put(key, theme, cs, usesTheme); 642 } 643 } 644 } 645 verifyPreloadConfig(@onfig int changingConfigurations, @Config int allowVarying, @AnyRes int resourceId, @Nullable String name)646 private boolean verifyPreloadConfig(@Config int changingConfigurations, 647 @Config int allowVarying, @AnyRes int resourceId, @Nullable String name) { 648 // We allow preloading of resources even if they vary by font scale (which 649 // doesn't impact resource selection) or density (which we handle specially by 650 // simply turning off all preloading), as well as any other configs specified 651 // by the caller. 652 if (((changingConfigurations&~(ActivityInfo.CONFIG_FONT_SCALE | 653 ActivityInfo.CONFIG_DENSITY)) & ~allowVarying) != 0) { 654 String resName; 655 try { 656 resName = getResourceName(resourceId); 657 } catch (NotFoundException e) { 658 resName = "?"; 659 } 660 // This should never happen in production, so we should log a 661 // warning even if we're not debugging. 662 Log.w(TAG, "Preloaded " + name + " resource #0x" 663 + Integer.toHexString(resourceId) 664 + " (" + resName + ") that varies with configuration!!"); 665 return false; 666 } 667 if (TRACE_FOR_PRELOAD) { 668 String resName; 669 try { 670 resName = getResourceName(resourceId); 671 } catch (NotFoundException e) { 672 resName = "?"; 673 } 674 Log.w(TAG, "Preloading " + name + " resource #0x" 675 + Integer.toHexString(resourceId) 676 + " (" + resName + ")"); 677 } 678 return true; 679 } 680 681 /** 682 * Loads a drawable from XML or resources stream. 683 */ loadDrawableForCookie(Resources wrapper, TypedValue value, int id, Resources.Theme theme)684 private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id, 685 Resources.Theme theme) { 686 if (value.string == null) { 687 throw new NotFoundException("Resource \"" + getResourceName(id) + "\" (" 688 + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value); 689 } 690 691 final String file = value.string.toString(); 692 693 if (TRACE_FOR_MISS_PRELOAD) { 694 // Log only framework resources 695 if ((id >>> 24) == 0x1) { 696 final String name = getResourceName(id); 697 if (name != null) { 698 Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id) 699 + ": " + name + " at " + file); 700 } 701 } 702 } 703 704 if (DEBUG_LOAD) { 705 Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file); 706 } 707 708 final Drawable dr; 709 710 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); 711 try { 712 if (file.endsWith(".xml")) { 713 final XmlResourceParser rp = loadXmlResourceParser( 714 file, id, value.assetCookie, "drawable"); 715 dr = Drawable.createFromXml(wrapper, rp, theme); 716 rp.close(); 717 } else { 718 final InputStream is = mAssets.openNonAsset( 719 value.assetCookie, file, AssetManager.ACCESS_STREAMING); 720 dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); 721 is.close(); 722 } 723 } catch (Exception e) { 724 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 725 final NotFoundException rnf = new NotFoundException( 726 "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); 727 rnf.initCause(e); 728 throw rnf; 729 } 730 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 731 732 return dr; 733 } 734 735 /** 736 * Given the value and id, we can get the XML filename as in value.data, based on that, we 737 * first try to load CSL from the cache. If not found, try to get from the constant state. 738 * Last, parse the XML and generate the CSL. 739 */ loadComplexColorFromName(Resources wrapper, Resources.Theme theme, TypedValue value, int id)740 private ComplexColor loadComplexColorFromName(Resources wrapper, Resources.Theme theme, 741 TypedValue value, int id) { 742 final long key = (((long) value.assetCookie) << 32) | value.data; 743 final ConfigurationBoundResourceCache<ComplexColor> cache = mComplexColorCache; 744 ComplexColor complexColor = cache.getInstance(key, wrapper, theme); 745 if (complexColor != null) { 746 return complexColor; 747 } 748 749 final android.content.res.ConstantState<ComplexColor> factory = 750 sPreloadedComplexColors.get(key); 751 752 if (factory != null) { 753 complexColor = factory.newInstance(wrapper, theme); 754 } 755 if (complexColor == null) { 756 complexColor = loadComplexColorForCookie(wrapper, value, id, theme); 757 } 758 759 if (complexColor != null) { 760 complexColor.setBaseChangingConfigurations(value.changingConfigurations); 761 762 if (mPreloading) { 763 if (verifyPreloadConfig(complexColor.getChangingConfigurations(), 764 0, value.resourceId, "color")) { 765 sPreloadedComplexColors.put(key, complexColor.getConstantState()); 766 } 767 } else { 768 cache.put(key, theme, complexColor.getConstantState()); 769 } 770 } 771 return complexColor; 772 } 773 774 @Nullable loadComplexColor(Resources wrapper, @NonNull TypedValue value, int id, Resources.Theme theme)775 ComplexColor loadComplexColor(Resources wrapper, @NonNull TypedValue value, int id, 776 Resources.Theme theme) { 777 if (TRACE_FOR_PRELOAD) { 778 // Log only framework resources 779 if ((id >>> 24) == 0x1) { 780 final String name = getResourceName(id); 781 if (name != null) android.util.Log.d("loadComplexColor", name); 782 } 783 } 784 785 final long key = (((long) value.assetCookie) << 32) | value.data; 786 787 // Handle inline color definitions. 788 if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT 789 && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { 790 return getColorStateListFromInt(value, key); 791 } 792 793 final String file = value.string.toString(); 794 795 ComplexColor complexColor; 796 if (file.endsWith(".xml")) { 797 try { 798 complexColor = loadComplexColorFromName(wrapper, theme, value, id); 799 } catch (Exception e) { 800 final NotFoundException rnf = new NotFoundException( 801 "File " + file + " from complex color resource ID #0x" 802 + Integer.toHexString(id)); 803 rnf.initCause(e); 804 throw rnf; 805 } 806 } else { 807 throw new NotFoundException( 808 "File " + file + " from drawable resource ID #0x" 809 + Integer.toHexString(id) + ": .xml extension required"); 810 } 811 812 return complexColor; 813 } 814 815 @Nullable loadColorStateList(Resources wrapper, TypedValue value, int id, Resources.Theme theme)816 ColorStateList loadColorStateList(Resources wrapper, TypedValue value, int id, 817 Resources.Theme theme) 818 throws NotFoundException { 819 if (TRACE_FOR_PRELOAD) { 820 // Log only framework resources 821 if ((id >>> 24) == 0x1) { 822 final String name = getResourceName(id); 823 if (name != null) android.util.Log.d("PreloadColorStateList", name); 824 } 825 } 826 827 final long key = (((long) value.assetCookie) << 32) | value.data; 828 829 // Handle inline color definitions. 830 if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT 831 && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { 832 return getColorStateListFromInt(value, key); 833 } 834 835 ComplexColor complexColor = loadComplexColorFromName(wrapper, theme, value, id); 836 if (complexColor != null && complexColor instanceof ColorStateList) { 837 return (ColorStateList) complexColor; 838 } 839 840 throw new NotFoundException( 841 "Can't find ColorStateList from drawable resource ID #0x" 842 + Integer.toHexString(id)); 843 } 844 845 @NonNull getColorStateListFromInt(@onNull TypedValue value, long key)846 private ColorStateList getColorStateListFromInt(@NonNull TypedValue value, long key) { 847 ColorStateList csl; 848 final android.content.res.ConstantState<ComplexColor> factory = 849 sPreloadedComplexColors.get(key); 850 if (factory != null) { 851 return (ColorStateList) factory.newInstance(); 852 } 853 854 csl = ColorStateList.valueOf(value.data); 855 856 if (mPreloading) { 857 if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId, 858 "color")) { 859 sPreloadedComplexColors.put(key, csl.getConstantState()); 860 } 861 } 862 863 return csl; 864 } 865 866 /** 867 * Load a ComplexColor based on the XML file content. The result can be a GradientColor or 868 * ColorStateList. Note that pure color will be wrapped into a ColorStateList. 869 * 870 * We deferred the parser creation to this function b/c we need to differentiate b/t gradient 871 * and selector tag. 872 * 873 * @return a ComplexColor (GradientColor or ColorStateList) based on the XML file content. 874 */ 875 @Nullable loadComplexColorForCookie(Resources wrapper, TypedValue value, int id, Resources.Theme theme)876 private ComplexColor loadComplexColorForCookie(Resources wrapper, TypedValue value, int id, 877 Resources.Theme theme) { 878 if (value.string == null) { 879 throw new UnsupportedOperationException( 880 "Can't convert to ComplexColor: type=0x" + value.type); 881 } 882 883 final String file = value.string.toString(); 884 885 if (TRACE_FOR_MISS_PRELOAD) { 886 // Log only framework resources 887 if ((id >>> 24) == 0x1) { 888 final String name = getResourceName(id); 889 if (name != null) { 890 Log.d(TAG, "Loading framework ComplexColor #" + Integer.toHexString(id) 891 + ": " + name + " at " + file); 892 } 893 } 894 } 895 896 if (DEBUG_LOAD) { 897 Log.v(TAG, "Loading ComplexColor for cookie " + value.assetCookie + ": " + file); 898 } 899 900 ComplexColor complexColor = null; 901 902 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); 903 if (file.endsWith(".xml")) { 904 try { 905 final XmlResourceParser parser = loadXmlResourceParser( 906 file, id, value.assetCookie, "ComplexColor"); 907 908 final AttributeSet attrs = Xml.asAttributeSet(parser); 909 int type; 910 while ((type = parser.next()) != XmlPullParser.START_TAG 911 && type != XmlPullParser.END_DOCUMENT) { 912 // Seek parser to start tag. 913 } 914 if (type != XmlPullParser.START_TAG) { 915 throw new XmlPullParserException("No start tag found"); 916 } 917 918 final String name = parser.getName(); 919 if (name.equals("gradient")) { 920 complexColor = GradientColor.createFromXmlInner(wrapper, parser, attrs, theme); 921 } else if (name.equals("selector")) { 922 complexColor = ColorStateList.createFromXmlInner(wrapper, parser, attrs, theme); 923 } 924 parser.close(); 925 } catch (Exception e) { 926 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 927 final NotFoundException rnf = new NotFoundException( 928 "File " + file + " from ComplexColor resource ID #0x" 929 + Integer.toHexString(id)); 930 rnf.initCause(e); 931 throw rnf; 932 } 933 } else { 934 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 935 throw new NotFoundException( 936 "File " + file + " from drawable resource ID #0x" 937 + Integer.toHexString(id) + ": .xml extension required"); 938 } 939 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 940 941 return complexColor; 942 } 943 944 /** 945 * Loads an XML parser for the specified file. 946 * 947 * @param file the path for the XML file to parse 948 * @param id the resource identifier for the file 949 * @param assetCookie the asset cookie for the file 950 * @param type the type of resource (used for logging) 951 * @return a parser for the specified XML file 952 * @throws NotFoundException if the file could not be loaded 953 */ 954 @NonNull loadXmlResourceParser(@onNull String file, @AnyRes int id, int assetCookie, @NonNull String type)955 XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie, 956 @NonNull String type) 957 throws NotFoundException { 958 if (id != 0) { 959 try { 960 synchronized (mCachedXmlBlocks) { 961 final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies; 962 final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles; 963 final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks; 964 // First see if this block is in our cache. 965 final int num = cachedXmlBlockFiles.length; 966 for (int i = 0; i < num; i++) { 967 if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null 968 && cachedXmlBlockFiles[i].equals(file)) { 969 return cachedXmlBlocks[i].newParser(); 970 } 971 } 972 973 // Not in the cache, create a new block and put it at 974 // the next slot in the cache. 975 final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file); 976 if (block != null) { 977 final int pos = (mLastCachedXmlBlockIndex + 1) % num; 978 mLastCachedXmlBlockIndex = pos; 979 final XmlBlock oldBlock = cachedXmlBlocks[pos]; 980 if (oldBlock != null) { 981 oldBlock.close(); 982 } 983 cachedXmlBlockCookies[pos] = assetCookie; 984 cachedXmlBlockFiles[pos] = file; 985 cachedXmlBlocks[pos] = block; 986 return block.newParser(); 987 } 988 } 989 } catch (Exception e) { 990 final NotFoundException rnf = new NotFoundException("File " + file 991 + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id)); 992 rnf.initCause(e); 993 throw rnf; 994 } 995 } 996 997 throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x" 998 + Integer.toHexString(id)); 999 } 1000 1001 /** 1002 * Start preloading of resource data using this Resources object. Only 1003 * for use by the zygote process for loading common system resources. 1004 * {@hide} 1005 */ startPreloading()1006 public final void startPreloading() { 1007 synchronized (sSync) { 1008 if (sPreloaded) { 1009 throw new IllegalStateException("Resources already preloaded"); 1010 } 1011 sPreloaded = true; 1012 mPreloading = true; 1013 mConfiguration.densityDpi = DisplayMetrics.DENSITY_DEVICE; 1014 updateConfiguration(null, null, null); 1015 } 1016 } 1017 1018 /** 1019 * Called by zygote when it is done preloading resources, to change back 1020 * to normal Resources operation. 1021 */ finishPreloading()1022 void finishPreloading() { 1023 if (mPreloading) { 1024 mPreloading = false; 1025 flushLayoutCache(); 1026 } 1027 } 1028 getPreloadedDrawables()1029 LongSparseArray<Drawable.ConstantState> getPreloadedDrawables() { 1030 return sPreloadedDrawables[0]; 1031 } 1032 newThemeImpl()1033 ThemeImpl newThemeImpl() { 1034 return new ThemeImpl(); 1035 } 1036 1037 /** 1038 * Creates a new ThemeImpl which is already set to the given Resources.ThemeKey. 1039 */ newThemeImpl(Resources.ThemeKey key)1040 ThemeImpl newThemeImpl(Resources.ThemeKey key) { 1041 ThemeImpl impl = new ThemeImpl(); 1042 impl.mKey.setTo(key); 1043 impl.rebase(); 1044 return impl; 1045 } 1046 1047 public class ThemeImpl { 1048 /** 1049 * Unique key for the series of styles applied to this theme. 1050 */ 1051 private final Resources.ThemeKey mKey = new Resources.ThemeKey(); 1052 1053 @SuppressWarnings("hiding") 1054 private final AssetManager mAssets; 1055 private final long mTheme; 1056 1057 /** 1058 * Resource identifier for the theme. 1059 */ 1060 private int mThemeResId = 0; 1061 ThemeImpl()1062 /*package*/ ThemeImpl() { 1063 mAssets = ResourcesImpl.this.mAssets; 1064 mTheme = mAssets.createTheme(); 1065 } 1066 1067 @Override finalize()1068 protected void finalize() throws Throwable { 1069 super.finalize(); 1070 mAssets.releaseTheme(mTheme); 1071 } 1072 getKey()1073 /*package*/ Resources.ThemeKey getKey() { 1074 return mKey; 1075 } 1076 getNativeTheme()1077 /*package*/ long getNativeTheme() { 1078 return mTheme; 1079 } 1080 getAppliedStyleResId()1081 /*package*/ int getAppliedStyleResId() { 1082 return mThemeResId; 1083 } 1084 applyStyle(int resId, boolean force)1085 void applyStyle(int resId, boolean force) { 1086 synchronized (mKey) { 1087 AssetManager.applyThemeStyle(mTheme, resId, force); 1088 1089 mThemeResId = resId; 1090 mKey.append(resId, force); 1091 } 1092 } 1093 setTo(ThemeImpl other)1094 void setTo(ThemeImpl other) { 1095 synchronized (mKey) { 1096 synchronized (other.mKey) { 1097 AssetManager.copyTheme(mTheme, other.mTheme); 1098 1099 mThemeResId = other.mThemeResId; 1100 mKey.setTo(other.getKey()); 1101 } 1102 } 1103 } 1104 1105 @NonNull obtainStyledAttributes(@onNull Resources.Theme wrapper, AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)1106 TypedArray obtainStyledAttributes(@NonNull Resources.Theme wrapper, 1107 AttributeSet set, 1108 @StyleableRes int[] attrs, 1109 @AttrRes int defStyleAttr, 1110 @StyleRes int defStyleRes) { 1111 synchronized (mKey) { 1112 final int len = attrs.length; 1113 final TypedArray array = TypedArray.obtain(wrapper.getResources(), len); 1114 1115 // XXX note that for now we only work with compiled XML files. 1116 // To support generic XML files we will need to manually parse 1117 // out the attributes from the XML file (applying type information 1118 // contained in the resources and such). 1119 final XmlBlock.Parser parser = (XmlBlock.Parser) set; 1120 AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes, 1121 parser != null ? parser.mParseState : 0, 1122 attrs, array.mData, array.mIndices); 1123 array.mTheme = wrapper; 1124 array.mXml = parser; 1125 1126 return array; 1127 } 1128 } 1129 1130 @NonNull resolveAttributes(@onNull Resources.Theme wrapper, @NonNull int[] values, @NonNull int[] attrs)1131 TypedArray resolveAttributes(@NonNull Resources.Theme wrapper, 1132 @NonNull int[] values, 1133 @NonNull int[] attrs) { 1134 synchronized (mKey) { 1135 final int len = attrs.length; 1136 if (values == null || len != values.length) { 1137 throw new IllegalArgumentException( 1138 "Base attribute values must the same length as attrs"); 1139 } 1140 1141 final TypedArray array = TypedArray.obtain(wrapper.getResources(), len); 1142 AssetManager.resolveAttrs(mTheme, 0, 0, values, attrs, array.mData, array.mIndices); 1143 array.mTheme = wrapper; 1144 array.mXml = null; 1145 return array; 1146 } 1147 } 1148 resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs)1149 boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) { 1150 synchronized (mKey) { 1151 return mAssets.getThemeValue(mTheme, resid, outValue, resolveRefs); 1152 } 1153 } 1154 getAllAttributes()1155 int[] getAllAttributes() { 1156 return mAssets.getStyleAttributes(getAppliedStyleResId()); 1157 } 1158 getChangingConfigurations()1159 @Config int getChangingConfigurations() { 1160 synchronized (mKey) { 1161 final int nativeChangingConfig = 1162 AssetManager.getThemeChangingConfigurations(mTheme); 1163 return ActivityInfo.activityInfoConfigNativeToJava(nativeChangingConfig); 1164 } 1165 } 1166 dump(int priority, String tag, String prefix)1167 public void dump(int priority, String tag, String prefix) { 1168 synchronized (mKey) { 1169 AssetManager.dumpTheme(mTheme, priority, tag, prefix); 1170 } 1171 } 1172 getTheme()1173 String[] getTheme() { 1174 synchronized (mKey) { 1175 final int N = mKey.mCount; 1176 final String[] themes = new String[N * 2]; 1177 for (int i = 0, j = N - 1; i < themes.length; i += 2, --j) { 1178 final int resId = mKey.mResId[j]; 1179 final boolean forced = mKey.mForce[j]; 1180 try { 1181 themes[i] = getResourceName(resId); 1182 } catch (NotFoundException e) { 1183 themes[i] = Integer.toHexString(i); 1184 } 1185 themes[i + 1] = forced ? "forced" : "not forced"; 1186 } 1187 return themes; 1188 } 1189 } 1190 1191 /** 1192 * Rebases the theme against the parent Resource object's current 1193 * configuration by re-applying the styles passed to 1194 * {@link #applyStyle(int, boolean)}. 1195 */ rebase()1196 void rebase() { 1197 synchronized (mKey) { 1198 AssetManager.clearTheme(mTheme); 1199 1200 // Reapply the same styles in the same order. 1201 for (int i = 0; i < mKey.mCount; i++) { 1202 final int resId = mKey.mResId[i]; 1203 final boolean force = mKey.mForce[i]; 1204 AssetManager.applyThemeStyle(mTheme, resId, force); 1205 } 1206 } 1207 } 1208 } 1209 } 1210