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