ResourcesImpl.java revision 991357fe25b3addabf85b871df3f4098fc4b833b
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        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration");
313        try {
314            synchronized (mAccessLock) {
315                if (false) {
316                    Slog.i(TAG, "**** Updating config of " + this + ": old config is "
317                            + mConfiguration + " old compat is " + mCompatibilityInfo);
318                    Slog.i(TAG, "**** Updating config of " + this + ": new config is "
319                            + config + " new compat is " + compat);
320                }
321                if (compat != null) {
322                    mCompatibilityInfo = compat;
323                }
324                if (metrics != null) {
325                    mMetrics.setTo(metrics);
326                }
327                // NOTE: We should re-arrange this code to create a Display
328                // with the CompatibilityInfo that is used everywhere we deal
329                // with the display in relation to this app, rather than
330                // doing the conversion here.  This impl should be okay because
331                // we make sure to return a compatible display in the places
332                // where there are public APIs to retrieve the display...  but
333                // it would be cleaner and more maintainble to just be
334                // consistently dealing with a compatible display everywhere in
335                // the framework.
336                mCompatibilityInfo.applyToDisplayMetrics(mMetrics);
337
338                final @Config int configChanges = calcConfigChanges(config);
339
340                LocaleList locales = mConfiguration.getLocales();
341                if (locales.isEmpty()) {
342                    locales = LocaleList.getAdjustedDefault();
343                    mConfiguration.setLocales(locales);
344                }
345                if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
346                    mMetrics.densityDpi = mConfiguration.densityDpi;
347                    mMetrics.density =
348                            mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
349                }
350                mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale;
351
352                final int width, height;
353                if (mMetrics.widthPixels >= mMetrics.heightPixels) {
354                    width = mMetrics.widthPixels;
355                    height = mMetrics.heightPixels;
356                } else {
357                    //noinspection SuspiciousNameCombination
358                    width = mMetrics.heightPixels;
359                    //noinspection SuspiciousNameCombination
360                    height = mMetrics.widthPixels;
361                }
362
363                final int keyboardHidden;
364                if (mConfiguration.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO
365                        && mConfiguration.hardKeyboardHidden
366                        == Configuration.HARDKEYBOARDHIDDEN_YES) {
367                    keyboardHidden = Configuration.KEYBOARDHIDDEN_SOFT;
368                } else {
369                    keyboardHidden = mConfiguration.keyboardHidden;
370                }
371
372                mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
373                        adjustLanguageTag(locales.get(0).toLanguageTag()),
374                        mConfiguration.orientation,
375                        mConfiguration.touchscreen,
376                        mConfiguration.densityDpi, mConfiguration.keyboard,
377                        keyboardHidden, mConfiguration.navigation, width, height,
378                        mConfiguration.smallestScreenWidthDp,
379                        mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
380                        mConfiguration.screenLayout, mConfiguration.uiMode,
381                        Build.VERSION.RESOURCES_SDK_INT);
382
383                if (DEBUG_CONFIG) {
384                    Slog.i(TAG, "**** Updating config of " + this + ": final config is "
385                            + mConfiguration + " final compat is " + mCompatibilityInfo);
386                }
387
388                mDrawableCache.onConfigurationChange(configChanges);
389                mColorDrawableCache.onConfigurationChange(configChanges);
390                mComplexColorCache.onConfigurationChange(configChanges);
391                mAnimatorCache.onConfigurationChange(configChanges);
392                mStateListAnimatorCache.onConfigurationChange(configChanges);
393
394                flushLayoutCache();
395            }
396            synchronized (sSync) {
397                if (mPluralRule != null) {
398                    mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().get(0));
399                }
400            }
401        } finally {
402            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
403        }
404    }
405
406    /**
407     * Applies the new configuration, returning a bitmask of the changes
408     * between the old and new configurations.
409     *
410     * @param config the new configuration
411     * @return bitmask of config changes
412     */
413    public @Config int calcConfigChanges(@Nullable Configuration config) {
414        if (config == null) {
415            // If there is no configuration, assume all flags have changed.
416            return 0xFFFFFFFF;
417        }
418
419        mTmpConfig.setTo(config);
420        int density = config.densityDpi;
421        if (density == Configuration.DENSITY_DPI_UNDEFINED) {
422            density = mMetrics.noncompatDensityDpi;
423        }
424
425        mCompatibilityInfo.applyToConfiguration(density, mTmpConfig);
426
427        if (mTmpConfig.getLocales().isEmpty()) {
428            mTmpConfig.setLocales(LocaleList.getDefault());
429        }
430        return mConfiguration.updateFrom(mTmpConfig);
431    }
432
433    /**
434     * {@code Locale.toLanguageTag} will transform the obsolete (and deprecated)
435     * language codes "in", "ji" and "iw" to "id", "yi" and "he" respectively.
436     *
437     * All released versions of android prior to "L" used the deprecated language
438     * tags, so we will need to support them for backwards compatibility.
439     *
440     * Note that this conversion needs to take place *after* the call to
441     * {@code toLanguageTag} because that will convert all the deprecated codes to
442     * the new ones, even if they're set manually.
443     */
444    private static String adjustLanguageTag(String languageTag) {
445        final int separator = languageTag.indexOf('-');
446        final String language;
447        final String remainder;
448
449        if (separator == -1) {
450            language = languageTag;
451            remainder = "";
452        } else {
453            language = languageTag.substring(0, separator);
454            remainder = languageTag.substring(separator);
455        }
456
457        return Locale.adjustLanguageCode(language) + remainder;
458    }
459
460    /**
461     * Call this to remove all cached loaded layout resources from the
462     * Resources object.  Only intended for use with performance testing
463     * tools.
464     */
465    public void flushLayoutCache() {
466        synchronized (mCachedXmlBlocks) {
467            Arrays.fill(mCachedXmlBlockCookies, 0);
468            Arrays.fill(mCachedXmlBlockFiles, null);
469
470            final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
471            for (int i = 0; i < XML_BLOCK_CACHE_SIZE; i++) {
472                final XmlBlock oldBlock = cachedXmlBlocks[i];
473                if (oldBlock != null) {
474                    oldBlock.close();
475                }
476            }
477            Arrays.fill(cachedXmlBlocks, null);
478        }
479    }
480
481    @Nullable
482    Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
483            boolean useCache) throws NotFoundException {
484        try {
485            if (TRACE_FOR_PRELOAD) {
486                // Log only framework resources
487                if ((id >>> 24) == 0x1) {
488                    final String name = getResourceName(id);
489                    if (name != null) {
490                        Log.d("PreloadDrawable", name);
491                    }
492                }
493            }
494
495            final boolean isColorDrawable;
496            final DrawableCache caches;
497            final long key;
498            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
499                    && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
500                isColorDrawable = true;
501                caches = mColorDrawableCache;
502                key = value.data;
503            } else {
504                isColorDrawable = false;
505                caches = mDrawableCache;
506                key = (((long) value.assetCookie) << 32) | value.data;
507            }
508
509            // First, check whether we have a cached version of this drawable
510            // that was inflated against the specified theme. Skip the cache if
511            // we're currently preloading or we're not using the cache.
512            if (!mPreloading && useCache) {
513                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
514                if (cachedDrawable != null) {
515                    return cachedDrawable;
516                }
517            }
518
519            // Next, check preloaded drawables. Preloaded drawables may contain
520            // unresolved theme attributes.
521            final Drawable.ConstantState cs;
522            if (isColorDrawable) {
523                cs = sPreloadedColorDrawables.get(key);
524            } else {
525                cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
526            }
527
528            Drawable dr;
529            if (cs != null) {
530                dr = cs.newDrawable(wrapper);
531            } else if (isColorDrawable) {
532                dr = new ColorDrawable(value.data);
533            } else {
534                dr = loadDrawableForCookie(wrapper, value, id, null);
535            }
536
537            // Determine if the drawable has unresolved theme attributes. If it
538            // does, we'll need to apply a theme and store it in a theme-specific
539            // cache.
540            final boolean canApplyTheme = dr != null && dr.canApplyTheme();
541            if (canApplyTheme && theme != null) {
542                dr = dr.mutate();
543                dr.applyTheme(theme);
544                dr.clearMutated();
545            }
546
547            // If we were able to obtain a drawable, store it in the appropriate
548            // cache: preload, not themed, null theme, or theme-specific. Don't
549            // pollute the cache with drawables loaded from a foreign density.
550            if (dr != null && useCache) {
551                dr.setChangingConfigurations(value.changingConfigurations);
552                cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
553            }
554
555            return dr;
556        } catch (Exception e) {
557            String name;
558            try {
559                name = getResourceName(id);
560            } catch (NotFoundException e2) {
561                name = "(missing name)";
562            }
563
564            // The target drawable might fail to load for any number of
565            // reasons, but we always want to include the resource name.
566            // Since the client already expects this method to throw a
567            // NotFoundException, just throw one of those.
568            final NotFoundException nfe = new NotFoundException("Drawable " + name
569                    + " with resource ID #0x" + Integer.toHexString(id), e);
570            nfe.setStackTrace(new StackTraceElement[0]);
571            throw nfe;
572        }
573    }
574
575    private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
576            Resources.Theme theme, boolean usesTheme, long key, Drawable dr) {
577        final Drawable.ConstantState cs = dr.getConstantState();
578        if (cs == null) {
579            return;
580        }
581
582        if (mPreloading) {
583            final int changingConfigs = cs.getChangingConfigurations();
584            if (isColorDrawable) {
585                if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
586                    sPreloadedColorDrawables.put(key, cs);
587                }
588            } else {
589                if (verifyPreloadConfig(
590                        changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
591                    if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
592                        // If this resource does not vary based on layout direction,
593                        // we can put it in all of the preload maps.
594                        sPreloadedDrawables[0].put(key, cs);
595                        sPreloadedDrawables[1].put(key, cs);
596                    } else {
597                        // Otherwise, only in the layout dir we loaded it for.
598                        sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
599                    }
600                }
601            }
602        } else {
603            synchronized (mAccessLock) {
604                caches.put(key, theme, cs, usesTheme);
605            }
606        }
607    }
608
609    private boolean verifyPreloadConfig(@Config int changingConfigurations,
610            @Config int allowVarying, @AnyRes int resourceId, @Nullable String name) {
611        // We allow preloading of resources even if they vary by font scale (which
612        // doesn't impact resource selection) or density (which we handle specially by
613        // simply turning off all preloading), as well as any other configs specified
614        // by the caller.
615        if (((changingConfigurations&~(ActivityInfo.CONFIG_FONT_SCALE |
616                ActivityInfo.CONFIG_DENSITY)) & ~allowVarying) != 0) {
617            String resName;
618            try {
619                resName = getResourceName(resourceId);
620            } catch (NotFoundException e) {
621                resName = "?";
622            }
623            // This should never happen in production, so we should log a
624            // warning even if we're not debugging.
625            Log.w(TAG, "Preloaded " + name + " resource #0x"
626                    + Integer.toHexString(resourceId)
627                    + " (" + resName + ") that varies with configuration!!");
628            return false;
629        }
630        if (TRACE_FOR_PRELOAD) {
631            String resName;
632            try {
633                resName = getResourceName(resourceId);
634            } catch (NotFoundException e) {
635                resName = "?";
636            }
637            Log.w(TAG, "Preloading " + name + " resource #0x"
638                    + Integer.toHexString(resourceId)
639                    + " (" + resName + ")");
640        }
641        return true;
642    }
643
644    /**
645     * Loads a drawable from XML or resources stream.
646     */
647    private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
648            Resources.Theme theme) {
649        if (value.string == null) {
650            throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
651                    + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value);
652        }
653
654        final String file = value.string.toString();
655
656        if (TRACE_FOR_MISS_PRELOAD) {
657            // Log only framework resources
658            if ((id >>> 24) == 0x1) {
659                final String name = getResourceName(id);
660                if (name != null) {
661                    Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id)
662                            + ": " + name + " at " + file);
663                }
664            }
665        }
666
667        if (DEBUG_LOAD) {
668            Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file);
669        }
670
671        final Drawable dr;
672
673        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
674        try {
675            if (file.endsWith(".xml")) {
676                final XmlResourceParser rp = loadXmlResourceParser(
677                        file, id, value.assetCookie, "drawable");
678                dr = Drawable.createFromXml(wrapper, rp, theme);
679                rp.close();
680            } else {
681                final InputStream is = mAssets.openNonAsset(
682                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
683                dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
684                is.close();
685            }
686        } catch (Exception e) {
687            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
688            final NotFoundException rnf = new NotFoundException(
689                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
690            rnf.initCause(e);
691            throw rnf;
692        }
693        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
694
695        return dr;
696    }
697
698    /**
699     * Given the value and id, we can get the XML filename as in value.data, based on that, we
700     * first try to load CSL from the cache. If not found, try to get from the constant state.
701     * Last, parse the XML and generate the CSL.
702     */
703    private ComplexColor loadComplexColorFromName(Resources wrapper, Resources.Theme theme,
704            TypedValue value, int id) {
705        final long key = (((long) value.assetCookie) << 32) | value.data;
706        final ConfigurationBoundResourceCache<ComplexColor> cache = mComplexColorCache;
707        ComplexColor complexColor = cache.getInstance(key, wrapper, theme);
708        if (complexColor != null) {
709            return complexColor;
710        }
711
712        final android.content.res.ConstantState<ComplexColor> factory =
713                sPreloadedComplexColors.get(key);
714
715        if (factory != null) {
716            complexColor = factory.newInstance(wrapper, theme);
717        }
718        if (complexColor == null) {
719            complexColor = loadComplexColorForCookie(wrapper, value, id, theme);
720        }
721
722        if (complexColor != null) {
723            complexColor.setBaseChangingConfigurations(value.changingConfigurations);
724
725            if (mPreloading) {
726                if (verifyPreloadConfig(complexColor.getChangingConfigurations(),
727                        0, value.resourceId, "color")) {
728                    sPreloadedComplexColors.put(key, complexColor.getConstantState());
729                }
730            } else {
731                cache.put(key, theme, complexColor.getConstantState());
732            }
733        }
734        return complexColor;
735    }
736
737    @Nullable
738    ComplexColor loadComplexColor(Resources wrapper, @NonNull TypedValue value, int id,
739            Resources.Theme theme) {
740        if (TRACE_FOR_PRELOAD) {
741            // Log only framework resources
742            if ((id >>> 24) == 0x1) {
743                final String name = getResourceName(id);
744                if (name != null) android.util.Log.d("loadComplexColor", name);
745            }
746        }
747
748        final long key = (((long) value.assetCookie) << 32) | value.data;
749
750        // Handle inline color definitions.
751        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
752                && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
753            return getColorStateListFromInt(value, key);
754        }
755
756        final String file = value.string.toString();
757
758        ComplexColor complexColor;
759        if (file.endsWith(".xml")) {
760            try {
761                complexColor = loadComplexColorFromName(wrapper, theme, value, id);
762            } catch (Exception e) {
763                final NotFoundException rnf = new NotFoundException(
764                        "File " + file + " from complex color resource ID #0x"
765                                + Integer.toHexString(id));
766                rnf.initCause(e);
767                throw rnf;
768            }
769        } else {
770            throw new NotFoundException(
771                    "File " + file + " from drawable resource ID #0x"
772                            + Integer.toHexString(id) + ": .xml extension required");
773        }
774
775        return complexColor;
776    }
777
778    @Nullable
779    ColorStateList loadColorStateList(Resources wrapper, TypedValue value, int id,
780            Resources.Theme theme)
781            throws NotFoundException {
782        if (TRACE_FOR_PRELOAD) {
783            // Log only framework resources
784            if ((id >>> 24) == 0x1) {
785                final String name = getResourceName(id);
786                if (name != null) android.util.Log.d("PreloadColorStateList", name);
787            }
788        }
789
790        final long key = (((long) value.assetCookie) << 32) | value.data;
791
792        // Handle inline color definitions.
793        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
794                && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
795            return getColorStateListFromInt(value, key);
796        }
797
798        ComplexColor complexColor = loadComplexColorFromName(wrapper, theme, value, id);
799        if (complexColor != null && complexColor instanceof ColorStateList) {
800            return (ColorStateList) complexColor;
801        }
802
803        throw new NotFoundException(
804                "Can't find ColorStateList from drawable resource ID #0x"
805                        + Integer.toHexString(id));
806    }
807
808    @NonNull
809    private ColorStateList getColorStateListFromInt(@NonNull TypedValue value, long key) {
810        ColorStateList csl;
811        final android.content.res.ConstantState<ComplexColor> factory =
812                sPreloadedComplexColors.get(key);
813        if (factory != null) {
814            return (ColorStateList) factory.newInstance();
815        }
816
817        csl = ColorStateList.valueOf(value.data);
818
819        if (mPreloading) {
820            if (verifyPreloadConfig(value.changingConfigurations, 0, value.resourceId,
821                    "color")) {
822                sPreloadedComplexColors.put(key, csl.getConstantState());
823            }
824        }
825
826        return csl;
827    }
828
829    /**
830     * Load a ComplexColor based on the XML file content. The result can be a GradientColor or
831     * ColorStateList. Note that pure color will be wrapped into a ColorStateList.
832     *
833     * We deferred the parser creation to this function b/c we need to differentiate b/t gradient
834     * and selector tag.
835     *
836     * @return a ComplexColor (GradientColor or ColorStateList) based on the XML file content.
837     */
838    @Nullable
839    private ComplexColor loadComplexColorForCookie(Resources wrapper, TypedValue value, int id,
840            Resources.Theme theme) {
841        if (value.string == null) {
842            throw new UnsupportedOperationException(
843                    "Can't convert to ComplexColor: type=0x" + value.type);
844        }
845
846        final String file = value.string.toString();
847
848        if (TRACE_FOR_MISS_PRELOAD) {
849            // Log only framework resources
850            if ((id >>> 24) == 0x1) {
851                final String name = getResourceName(id);
852                if (name != null) {
853                    Log.d(TAG, "Loading framework ComplexColor #" + Integer.toHexString(id)
854                            + ": " + name + " at " + file);
855                }
856            }
857        }
858
859        if (DEBUG_LOAD) {
860            Log.v(TAG, "Loading ComplexColor for cookie " + value.assetCookie + ": " + file);
861        }
862
863        ComplexColor complexColor = null;
864
865        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
866        if (file.endsWith(".xml")) {
867            try {
868                final XmlResourceParser parser = loadXmlResourceParser(
869                        file, id, value.assetCookie, "ComplexColor");
870
871                final AttributeSet attrs = Xml.asAttributeSet(parser);
872                int type;
873                while ((type = parser.next()) != XmlPullParser.START_TAG
874                        && type != XmlPullParser.END_DOCUMENT) {
875                    // Seek parser to start tag.
876                }
877                if (type != XmlPullParser.START_TAG) {
878                    throw new XmlPullParserException("No start tag found");
879                }
880
881                final String name = parser.getName();
882                if (name.equals("gradient")) {
883                    complexColor = GradientColor.createFromXmlInner(wrapper, parser, attrs, theme);
884                } else if (name.equals("selector")) {
885                    complexColor = ColorStateList.createFromXmlInner(wrapper, parser, attrs, theme);
886                }
887                parser.close();
888            } catch (Exception e) {
889                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
890                final NotFoundException rnf = new NotFoundException(
891                        "File " + file + " from ComplexColor resource ID #0x"
892                                + Integer.toHexString(id));
893                rnf.initCause(e);
894                throw rnf;
895            }
896        } else {
897            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
898            throw new NotFoundException(
899                    "File " + file + " from drawable resource ID #0x"
900                            + Integer.toHexString(id) + ": .xml extension required");
901        }
902        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
903
904        return complexColor;
905    }
906
907    /**
908     * Loads an XML parser for the specified file.
909     *
910     * @param file the path for the XML file to parse
911     * @param id the resource identifier for the file
912     * @param assetCookie the asset cookie for the file
913     * @param type the type of resource (used for logging)
914     * @return a parser for the specified XML file
915     * @throws NotFoundException if the file could not be loaded
916     */
917    @NonNull
918    XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
919            @NonNull String type)
920            throws NotFoundException {
921        if (id != 0) {
922            try {
923                synchronized (mCachedXmlBlocks) {
924                    final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
925                    final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
926                    final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
927                    // First see if this block is in our cache.
928                    final int num = cachedXmlBlockFiles.length;
929                    for (int i = 0; i < num; i++) {
930                        if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
931                                && cachedXmlBlockFiles[i].equals(file)) {
932                            return cachedXmlBlocks[i].newParser();
933                        }
934                    }
935
936                    // Not in the cache, create a new block and put it at
937                    // the next slot in the cache.
938                    final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
939                    if (block != null) {
940                        final int pos = (mLastCachedXmlBlockIndex + 1) % num;
941                        mLastCachedXmlBlockIndex = pos;
942                        final XmlBlock oldBlock = cachedXmlBlocks[pos];
943                        if (oldBlock != null) {
944                            oldBlock.close();
945                        }
946                        cachedXmlBlockCookies[pos] = assetCookie;
947                        cachedXmlBlockFiles[pos] = file;
948                        cachedXmlBlocks[pos] = block;
949                        return block.newParser();
950                    }
951                }
952            } catch (Exception e) {
953                final NotFoundException rnf = new NotFoundException("File " + file
954                        + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id));
955                rnf.initCause(e);
956                throw rnf;
957            }
958        }
959
960        throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x"
961                + Integer.toHexString(id));
962    }
963
964    /**
965     * Start preloading of resource data using this Resources object.  Only
966     * for use by the zygote process for loading common system resources.
967     * {@hide}
968     */
969    public final void startPreloading() {
970        synchronized (sSync) {
971            if (sPreloaded) {
972                throw new IllegalStateException("Resources already preloaded");
973            }
974            sPreloaded = true;
975            mPreloading = true;
976            mConfiguration.densityDpi = DisplayMetrics.DENSITY_DEVICE;
977            updateConfiguration(null, null, null);
978        }
979    }
980
981    /**
982     * Called by zygote when it is done preloading resources, to change back
983     * to normal Resources operation.
984     */
985    void finishPreloading() {
986        if (mPreloading) {
987            mPreloading = false;
988            flushLayoutCache();
989        }
990    }
991
992    LongSparseArray<Drawable.ConstantState> getPreloadedDrawables() {
993        return sPreloadedDrawables[0];
994    }
995
996    ThemeImpl newThemeImpl() {
997        return new ThemeImpl();
998    }
999
1000    /**
1001     * Creates a new ThemeImpl which is already set to the given Resources.ThemeKey.
1002     */
1003    ThemeImpl newThemeImpl(Resources.ThemeKey key) {
1004        ThemeImpl impl = new ThemeImpl();
1005        impl.mKey.setTo(key);
1006        impl.rebase();
1007        return impl;
1008    }
1009
1010    public class ThemeImpl {
1011        /**
1012         * Unique key for the series of styles applied to this theme.
1013         */
1014        private final Resources.ThemeKey mKey = new Resources.ThemeKey();
1015
1016        @SuppressWarnings("hiding")
1017        private final AssetManager mAssets;
1018        private final long mTheme;
1019
1020        /**
1021         * Resource identifier for the theme.
1022         */
1023        private int mThemeResId = 0;
1024
1025        /*package*/ ThemeImpl() {
1026            mAssets = ResourcesImpl.this.mAssets;
1027            mTheme = mAssets.createTheme();
1028        }
1029
1030        @Override
1031        protected void finalize() throws Throwable {
1032            super.finalize();
1033            mAssets.releaseTheme(mTheme);
1034        }
1035
1036        /*package*/ Resources.ThemeKey getKey() {
1037            return mKey;
1038        }
1039
1040        /*package*/ long getNativeTheme() {
1041            return mTheme;
1042        }
1043
1044        /*package*/ int getAppliedStyleResId() {
1045            return mThemeResId;
1046        }
1047
1048        void applyStyle(int resId, boolean force) {
1049            synchronized (mKey) {
1050                AssetManager.applyThemeStyle(mTheme, resId, force);
1051
1052                mThemeResId = resId;
1053                mKey.append(resId, force);
1054            }
1055        }
1056
1057        void setTo(ThemeImpl other) {
1058            synchronized (mKey) {
1059                synchronized (other.mKey) {
1060                    AssetManager.copyTheme(mTheme, other.mTheme);
1061
1062                    mThemeResId = other.mThemeResId;
1063                    mKey.setTo(other.getKey());
1064                }
1065            }
1066        }
1067
1068        @NonNull
1069        TypedArray obtainStyledAttributes(@NonNull Resources.Theme wrapper,
1070                AttributeSet set,
1071                @StyleableRes int[] attrs,
1072                @AttrRes int defStyleAttr,
1073                @StyleRes int defStyleRes) {
1074            synchronized (mKey) {
1075                final int len = attrs.length;
1076                final TypedArray array = TypedArray.obtain(wrapper.getResources(), len);
1077
1078                // XXX note that for now we only work with compiled XML files.
1079                // To support generic XML files we will need to manually parse
1080                // out the attributes from the XML file (applying type information
1081                // contained in the resources and such).
1082                final XmlBlock.Parser parser = (XmlBlock.Parser) set;
1083                AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,
1084                        parser != null ? parser.mParseState : 0,
1085                        attrs, array.mData, array.mIndices);
1086                array.mTheme = wrapper;
1087                array.mXml = parser;
1088
1089                return array;
1090            }
1091        }
1092
1093        @NonNull
1094        TypedArray resolveAttributes(@NonNull Resources.Theme wrapper,
1095                @NonNull int[] values,
1096                @NonNull int[] attrs) {
1097            synchronized (mKey) {
1098                final int len = attrs.length;
1099                if (values == null || len != values.length) {
1100                    throw new IllegalArgumentException(
1101                            "Base attribute values must the same length as attrs");
1102                }
1103
1104                final TypedArray array = TypedArray.obtain(wrapper.getResources(), len);
1105                AssetManager.resolveAttrs(mTheme, 0, 0, values, attrs, array.mData, array.mIndices);
1106                array.mTheme = wrapper;
1107                array.mXml = null;
1108                return array;
1109            }
1110        }
1111
1112        boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) {
1113            synchronized (mKey) {
1114                return mAssets.getThemeValue(mTheme, resid, outValue, resolveRefs);
1115            }
1116        }
1117
1118        int[] getAllAttributes() {
1119            return mAssets.getStyleAttributes(getAppliedStyleResId());
1120        }
1121
1122        @Config int getChangingConfigurations() {
1123            synchronized (mKey) {
1124                final int nativeChangingConfig =
1125                        AssetManager.getThemeChangingConfigurations(mTheme);
1126                return ActivityInfo.activityInfoConfigNativeToJava(nativeChangingConfig);
1127            }
1128        }
1129
1130        public void dump(int priority, String tag, String prefix) {
1131            synchronized (mKey) {
1132                AssetManager.dumpTheme(mTheme, priority, tag, prefix);
1133            }
1134        }
1135
1136        String[] getTheme() {
1137            synchronized (mKey) {
1138                final int N = mKey.mCount;
1139                final String[] themes = new String[N * 2];
1140                for (int i = 0, j = N - 1; i < themes.length; i += 2, --j) {
1141                    final int resId = mKey.mResId[j];
1142                    final boolean forced = mKey.mForce[j];
1143                    try {
1144                        themes[i] = getResourceName(resId);
1145                    } catch (NotFoundException e) {
1146                        themes[i] = Integer.toHexString(i);
1147                    }
1148                    themes[i + 1] = forced ? "forced" : "not forced";
1149                }
1150                return themes;
1151            }
1152        }
1153
1154        /**
1155         * Rebases the theme against the parent Resource object's current
1156         * configuration by re-applying the styles passed to
1157         * {@link #applyStyle(int, boolean)}.
1158         */
1159        void rebase() {
1160            synchronized (mKey) {
1161                AssetManager.clearTheme(mTheme);
1162
1163                // Reapply the same styles in the same order.
1164                for (int i = 0; i < mKey.mCount; i++) {
1165                    final int resId = mKey.mResId[i];
1166                    final boolean force = mKey.mForce[i];
1167                    AssetManager.applyThemeStyle(mTheme, resId, force);
1168                }
1169            }
1170        }
1171    }
1172}
1173