1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.content.res;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.graphics.drawable.Drawable;
24import android.os.Build;
25import android.support.annotation.ColorRes;
26import android.support.annotation.DrawableRes;
27import android.support.annotation.NonNull;
28import android.support.annotation.Nullable;
29import android.support.v4.content.ContextCompat;
30import android.support.v7.widget.AppCompatDrawableManager;
31import android.util.Log;
32import android.util.SparseArray;
33import android.util.TypedValue;
34
35import org.xmlpull.v1.XmlPullParser;
36
37import java.util.WeakHashMap;
38
39/**
40 * Class for accessing an application's resources through AppCompat, and thus any backward
41 * compatible functionality.
42 */
43public final class AppCompatResources {
44
45    private static final String LOG_TAG = "AppCompatResources";
46    private static final ThreadLocal<TypedValue> TL_TYPED_VALUE = new ThreadLocal<>();
47
48    private static final WeakHashMap<Context, SparseArray<ColorStateListCacheEntry>>
49            sColorStateCaches = new WeakHashMap<>(0);
50
51    private static final Object sColorStateCacheLock = new Object();
52
53    private AppCompatResources() {}
54
55    /**
56     * Returns the {@link ColorStateList} from the given resource. The resource can include
57     * themeable attributes, regardless of API level.
58     *
59     * @param context context to inflate against
60     * @param resId the resource identifier of the ColorStateList to retrieve
61     */
62    public static ColorStateList getColorStateList(@NonNull Context context, @ColorRes int resId) {
63        if (Build.VERSION.SDK_INT >= 23) {
64            // On M+ we can use the framework
65            return context.getColorStateList(resId);
66        }
67
68        // Before that, we'll try handle it ourselves
69        ColorStateList csl = getCachedColorStateList(context, resId);
70        if (csl != null) {
71            return csl;
72        }
73        // Cache miss, so try and inflate it ourselves
74        csl = inflateColorStateList(context, resId);
75        if (csl != null) {
76            // If we inflated it, add it to the cache and return
77            addColorStateListToCache(context, resId, csl);
78            return csl;
79        }
80
81        // If we reach here then we couldn't inflate it, so let the framework handle it
82        return ContextCompat.getColorStateList(context, resId);
83    }
84
85    /**
86     * Return a drawable object associated with a particular resource ID.
87     *
88     * <p>This method supports inflation of {@code <vector>} and {@code <animated-vector>}
89     * resources on devices where platform support is not available.</p>
90     *
91     * @param context context to inflate against
92     * @param resId   The desired resource identifier, as generated by the aapt
93     *                tool. This integer encodes the package, type, and resource
94     *                entry. The value 0 is an invalid identifier.
95     * @return Drawable An object that can be used to draw this resource.
96     * @see ContextCompat#getDrawable(Context, int)
97     */
98    @Nullable
99    public static Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
100        return AppCompatDrawableManager.get().getDrawable(context, resId);
101    }
102
103    /**
104     * Inflates a {@link ColorStateList} from resources, honouring theme attributes.
105     */
106    @Nullable
107    private static ColorStateList inflateColorStateList(Context context, int resId) {
108        if (isColorInt(context, resId)) {
109            // The resource is a color int, we can't handle it so return null
110            return null;
111        }
112
113        final Resources r = context.getResources();
114        final XmlPullParser xml = r.getXml(resId);
115        try {
116            return AppCompatColorStateListInflater.createFromXml(r, xml, context.getTheme());
117        } catch (Exception e) {
118            Log.e(LOG_TAG, "Failed to inflate ColorStateList, leaving it to the framework", e);
119        }
120        return null;
121    }
122
123    @Nullable
124    private static ColorStateList getCachedColorStateList(@NonNull Context context,
125            @ColorRes int resId) {
126        synchronized (sColorStateCacheLock) {
127            final SparseArray<ColorStateListCacheEntry> entries = sColorStateCaches.get(context);
128            if (entries != null && entries.size() > 0) {
129                final ColorStateListCacheEntry entry = entries.get(resId);
130                if (entry != null) {
131                    if (entry.configuration.equals(context.getResources().getConfiguration())) {
132                        // If the current configuration matches the entry's, we can use it
133                        return entry.value;
134                    } else {
135                        // Otherwise we'll remove the entry
136                        entries.remove(resId);
137                    }
138                }
139            }
140        }
141        return null;
142    }
143
144    private static void addColorStateListToCache(@NonNull Context context, @ColorRes int resId,
145            @NonNull ColorStateList value) {
146        synchronized (sColorStateCacheLock) {
147            SparseArray<ColorStateListCacheEntry> entries = sColorStateCaches.get(context);
148            if (entries == null) {
149                entries = new SparseArray<>();
150                sColorStateCaches.put(context, entries);
151            }
152            entries.append(resId, new ColorStateListCacheEntry(value,
153                    context.getResources().getConfiguration()));
154        }
155    }
156
157    private static boolean isColorInt(@NonNull Context context, @ColorRes int resId) {
158        final Resources r = context.getResources();
159
160        final TypedValue value = getTypedValue();
161        r.getValue(resId, value, true);
162
163        return value.type >= TypedValue.TYPE_FIRST_COLOR_INT
164                && value.type <= TypedValue.TYPE_LAST_COLOR_INT;
165    }
166
167    @NonNull
168    private static TypedValue getTypedValue() {
169        TypedValue tv = TL_TYPED_VALUE.get();
170        if (tv == null) {
171            tv = new TypedValue();
172            TL_TYPED_VALUE.set(tv);
173        }
174        return tv;
175    }
176
177    private static class ColorStateListCacheEntry {
178        final ColorStateList value;
179        final Configuration configuration;
180
181        ColorStateListCacheEntry(@NonNull ColorStateList value,
182                @NonNull Configuration configuration) {
183            this.value = value;
184            this.configuration = configuration;
185        }
186    }
187
188}
189