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