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