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 */ 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 149 public DisplayAdjustments getDisplayAdjustments() { 150 return mDisplayAdjustments; 151 } 152 153 public AssetManager getAssets() { 154 return mAssets; 155 } 156 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 163 Configuration getConfiguration() { 164 return mConfiguration; 165 } 166 167 Configuration[] getSizeConfigurations() { 168 return mAssets.getSizeConfigurations(); 169 } 170 171 CompatibilityInfo getCompatibilityInfo() { 172 return mDisplayAdjustments.getCompatibilityInfo(); 173 } 174 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 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 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 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 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 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 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 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 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 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 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 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 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 312 ConfigurationBoundResourceCache<Animator> getAnimatorCache() { 313 return mAnimatorCache; 314 } 315 316 ConfigurationBoundResourceCache<StateListAnimator> getStateListAnimatorCache() { 317 return mStateListAnimatorCache; 318 } 319 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 */ 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 */ 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 */ 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 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 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 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 */ 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 */ 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 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 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 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 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 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 */ 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 */ 1022 void finishPreloading() { 1023 if (mPreloading) { 1024 mPreloading = false; 1025 flushLayoutCache(); 1026 } 1027 } 1028 1029 LongSparseArray<Drawable.ConstantState> getPreloadedDrawables() { 1030 return sPreloadedDrawables[0]; 1031 } 1032 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 */ 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 1062 /*package*/ ThemeImpl() { 1063 mAssets = ResourcesImpl.this.mAssets; 1064 mTheme = mAssets.createTheme(); 1065 } 1066 1067 @Override 1068 protected void finalize() throws Throwable { 1069 super.finalize(); 1070 mAssets.releaseTheme(mTheme); 1071 } 1072 1073 /*package*/ Resources.ThemeKey getKey() { 1074 return mKey; 1075 } 1076 1077 /*package*/ long getNativeTheme() { 1078 return mTheme; 1079 } 1080 1081 /*package*/ int getAppliedStyleResId() { 1082 return mThemeResId; 1083 } 1084 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 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 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 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 1149 boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) { 1150 synchronized (mKey) { 1151 return mAssets.getThemeValue(mTheme, resid, outValue, resolveRefs); 1152 } 1153 } 1154 1155 int[] getAllAttributes() { 1156 return mAssets.getStyleAttributes(getAppliedStyleResId()); 1157 } 1158 1159 @Config int getChangingConfigurations() { 1160 synchronized (mKey) { 1161 final int nativeChangingConfig = 1162 AssetManager.getThemeChangingConfigurations(mTheme); 1163 return ActivityInfo.activityInfoConfigNativeToJava(nativeChangingConfig); 1164 } 1165 } 1166 1167 public void dump(int priority, String tag, String prefix) { 1168 synchronized (mKey) { 1169 AssetManager.dumpTheme(mTheme, priority, tag, prefix); 1170 } 1171 } 1172 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 */ 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