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 androidx.appcompat.app;
18
19import android.content.res.Resources;
20import android.os.Build;
21import android.util.Log;
22import android.util.LongSparseArray;
23
24import androidx.annotation.NonNull;
25import androidx.annotation.RequiresApi;
26
27import java.lang.reflect.Field;
28import java.util.Map;
29
30class ResourcesFlusher {
31    private static final String TAG = "ResourcesFlusher";
32
33    private static Field sDrawableCacheField;
34    private static boolean sDrawableCacheFieldFetched;
35
36    private static Class sThemedResourceCacheClazz;
37    private static boolean sThemedResourceCacheClazzFetched;
38
39    private static Field sThemedResourceCache_mUnthemedEntriesField;
40    private static boolean sThemedResourceCache_mUnthemedEntriesFieldFetched;
41
42    private static Field sResourcesImplField;
43    private static boolean sResourcesImplFieldFetched;
44
45    static boolean flush(@NonNull final Resources resources) {
46        if (Build.VERSION.SDK_INT >= 24) {
47            return flushNougats(resources);
48        } else if (Build.VERSION.SDK_INT >= 23) {
49            return flushMarshmallows(resources);
50        } else if (Build.VERSION.SDK_INT >= 21) {
51            return flushLollipops(resources);
52        }
53        return false;
54    }
55
56    @RequiresApi(21)
57    private static boolean flushLollipops(@NonNull final Resources resources) {
58        if (!sDrawableCacheFieldFetched) {
59            try {
60                sDrawableCacheField = Resources.class.getDeclaredField("mDrawableCache");
61                sDrawableCacheField.setAccessible(true);
62            } catch (NoSuchFieldException e) {
63                Log.e(TAG, "Could not retrieve Resources#mDrawableCache field", e);
64            }
65            sDrawableCacheFieldFetched = true;
66        }
67        if (sDrawableCacheField != null) {
68            Map drawableCache = null;
69            try {
70                drawableCache = (Map) sDrawableCacheField.get(resources);
71            } catch (IllegalAccessException e) {
72                Log.e(TAG, "Could not retrieve value from Resources#mDrawableCache", e);
73            }
74            if (drawableCache != null) {
75                drawableCache.clear();
76                return true;
77            }
78        }
79        return false;
80    }
81
82    @RequiresApi(23)
83    private static boolean flushMarshmallows(@NonNull final Resources resources) {
84        if (!sDrawableCacheFieldFetched) {
85            try {
86                sDrawableCacheField = Resources.class.getDeclaredField("mDrawableCache");
87                sDrawableCacheField.setAccessible(true);
88            } catch (NoSuchFieldException e) {
89                Log.e(TAG, "Could not retrieve Resources#mDrawableCache field", e);
90            }
91            sDrawableCacheFieldFetched = true;
92        }
93
94        Object drawableCache = null;
95        if (sDrawableCacheField != null) {
96            try {
97                drawableCache = sDrawableCacheField.get(resources);
98            } catch (IllegalAccessException e) {
99                Log.e(TAG, "Could not retrieve value from Resources#mDrawableCache", e);
100            }
101        }
102
103        if (drawableCache == null) {
104            // If there is no drawable cache, there's nothing to flush...
105            return false;
106        }
107
108        return drawableCache != null && flushThemedResourcesCache(drawableCache);
109    }
110
111    @RequiresApi(24)
112    private static boolean flushNougats(@NonNull final Resources resources) {
113        if (!sResourcesImplFieldFetched) {
114            try {
115                sResourcesImplField = Resources.class.getDeclaredField("mResourcesImpl");
116                sResourcesImplField.setAccessible(true);
117            } catch (NoSuchFieldException e) {
118                Log.e(TAG, "Could not retrieve Resources#mResourcesImpl field", e);
119            }
120            sResourcesImplFieldFetched = true;
121        }
122
123        if (sResourcesImplField == null) {
124            // If the mResourcesImpl field isn't available, bail out now
125            return false;
126        }
127
128        Object resourcesImpl = null;
129        try {
130            resourcesImpl = sResourcesImplField.get(resources);
131        } catch (IllegalAccessException e) {
132            Log.e(TAG, "Could not retrieve value from Resources#mResourcesImpl", e);
133        }
134
135        if (resourcesImpl == null) {
136            // If there is no impl instance, bail out now
137            return false;
138        }
139
140        if (!sDrawableCacheFieldFetched) {
141            try {
142                sDrawableCacheField = resourcesImpl.getClass().getDeclaredField("mDrawableCache");
143                sDrawableCacheField.setAccessible(true);
144            } catch (NoSuchFieldException e) {
145                Log.e(TAG, "Could not retrieve ResourcesImpl#mDrawableCache field", e);
146            }
147            sDrawableCacheFieldFetched = true;
148        }
149
150        Object drawableCache = null;
151        if (sDrawableCacheField != null) {
152            try {
153                drawableCache = sDrawableCacheField.get(resourcesImpl);
154            } catch (IllegalAccessException e) {
155                Log.e(TAG, "Could not retrieve value from ResourcesImpl#mDrawableCache", e);
156            }
157        }
158
159        return drawableCache != null && flushThemedResourcesCache(drawableCache);
160    }
161
162    @RequiresApi(16)
163    private static boolean flushThemedResourcesCache(@NonNull final Object cache) {
164        if (!sThemedResourceCacheClazzFetched) {
165            try {
166                sThemedResourceCacheClazz = Class.forName("android.content.res.ThemedResourceCache");
167            } catch (ClassNotFoundException e) {
168                Log.e(TAG, "Could not find ThemedResourceCache class", e);
169            }
170            sThemedResourceCacheClazzFetched = true;
171        }
172
173        if (sThemedResourceCacheClazz == null) {
174            // If the ThemedResourceCache class isn't available, bail out now
175            return false;
176        }
177
178        if (!sThemedResourceCache_mUnthemedEntriesFieldFetched) {
179            try {
180                sThemedResourceCache_mUnthemedEntriesField =
181                        sThemedResourceCacheClazz.getDeclaredField("mUnthemedEntries");
182                sThemedResourceCache_mUnthemedEntriesField.setAccessible(true);
183            } catch (NoSuchFieldException ee) {
184                Log.e(TAG, "Could not retrieve ThemedResourceCache#mUnthemedEntries field", ee);
185            }
186            sThemedResourceCache_mUnthemedEntriesFieldFetched = true;
187        }
188
189        if (sThemedResourceCache_mUnthemedEntriesField == null) {
190            // Didn't get mUnthemedEntries field, bail out...
191            return false;
192        }
193
194        LongSparseArray unthemedEntries = null;
195        try {
196            unthemedEntries = (LongSparseArray)
197                    sThemedResourceCache_mUnthemedEntriesField.get(cache);
198        } catch (IllegalAccessException e) {
199            Log.e(TAG, "Could not retrieve value from ThemedResourceCache#mUnthemedEntries", e);
200        }
201
202        if (unthemedEntries != null) {
203            unthemedEntries.clear();
204            return true;
205        }
206        return false;
207    }
208
209    private ResourcesFlusher() {
210    }
211}
212