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