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