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