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