1 /*
2  * Copyright (C) 2015 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 
17 package android.content.res;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.pm.ActivityInfo.Config;
23 import android.content.res.Resources.Theme;
24 import android.content.res.Resources.ThemeKey;
25 import android.util.ArrayMap;
26 import android.util.LongSparseArray;
27 
28 import java.lang.ref.WeakReference;
29 
30 /**
31  * Data structure used for caching data against themes.
32  *
33  * @param <T> type of data to cache
34  */
35 abstract class ThemedResourceCache<T> {
36     public static final int UNDEFINED_GENERATION = -1;
37     @UnsupportedAppUsage
38     private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
39     private LongSparseArray<WeakReference<T>> mUnthemedEntries;
40     private LongSparseArray<WeakReference<T>> mNullThemedEntries;
41 
42     private int mGeneration;
43 
44     /**
45      * Adds a new theme-dependent entry to the cache.
46      *
47      * @param key a key that uniquely identifies the entry
48      * @param theme the theme against which this entry was inflated, or
49      *              {@code null} if the entry has no theme applied
50      * @param entry the entry to cache
51      * @param generation The generation of the cache to compare against before storing
52      */
put(long key, @Nullable Theme theme, @NonNull T entry, int generation)53     public void put(long key, @Nullable Theme theme, @NonNull T entry, int generation) {
54         put(key, theme, entry, generation, true);
55     }
56 
57     /**
58      * Adds a new entry to the cache.
59      *
60      * @param key a key that uniquely identifies the entry
61      * @param theme the theme against which this entry was inflated, or
62      *              {@code null} if the entry has no theme applied
63      * @param entry the entry to cache
64      * @param generation The generation of the cache to compare against before storing
65      * @param usesTheme {@code true} if the entry is affected theme changes,
66      *                  {@code false} otherwise
67      */
put(long key, @Nullable Theme theme, @NonNull T entry, int generation, boolean usesTheme)68     public void put(long key, @Nullable Theme theme, @NonNull T entry, int generation,
69             boolean usesTheme) {
70         if (entry == null) {
71             return;
72         }
73 
74         synchronized (this) {
75             final LongSparseArray<WeakReference<T>> entries;
76             if (!usesTheme) {
77                 entries = getUnthemedLocked(true);
78             } else {
79                 entries = getThemedLocked(theme, true);
80             }
81             if (entries != null
82                     && ((generation == mGeneration) || (generation == UNDEFINED_GENERATION)))  {
83                 entries.put(key, new WeakReference<>(entry));
84             }
85         }
86     }
87 
88     /**
89      * Returns the current generation of the cache
90      *
91      * @return The current generation
92      */
getGeneration()93     public int getGeneration() {
94         return mGeneration;
95     }
96 
97     /**
98      * Returns an entry from the cache.
99      *
100      * @param key a key that uniquely identifies the entry
101      * @param theme the theme where the entry will be used
102      * @return a cached entry, or {@code null} if not in the cache
103      */
104     @Nullable
get(long key, @Nullable Theme theme)105     public T get(long key, @Nullable Theme theme) {
106         // The themed (includes null-themed) and unthemed caches are mutually
107         // exclusive, so we'll give priority to whichever one we think we'll
108         // hit first. Since most of the framework drawables are themed, that's
109         // probably going to be the themed cache.
110         synchronized (this) {
111             final LongSparseArray<WeakReference<T>> themedEntries = getThemedLocked(theme, false);
112             if (themedEntries != null) {
113                 final WeakReference<T> themedEntry = themedEntries.get(key);
114                 if (themedEntry != null) {
115                     return themedEntry.get();
116                 }
117             }
118 
119             final LongSparseArray<WeakReference<T>> unthemedEntries = getUnthemedLocked(false);
120             if (unthemedEntries != null) {
121                 final WeakReference<T> unthemedEntry = unthemedEntries.get(key);
122                 if (unthemedEntry != null) {
123                     return unthemedEntry.get();
124                 }
125             }
126         }
127 
128         return null;
129     }
130 
131 
132     /**
133      * Prunes cache entries that have been invalidated by a configuration
134      * change.
135      *
136      * @param configChanges a bitmask of configuration changes
137      */
138     @UnsupportedAppUsage
onConfigurationChange(@onfig int configChanges)139     public void onConfigurationChange(@Config int configChanges) {
140         synchronized (this) {
141             pruneLocked(configChanges);
142             mGeneration++;
143         }
144     }
145 
146     /**
147      * Returns whether a cached entry has been invalidated by a configuration
148      * change.
149      *
150      * @param entry a cached entry
151      * @param configChanges a non-zero bitmask of configuration changes
152      * @return {@code true} if the entry is invalid, {@code false} otherwise
153      */
shouldInvalidateEntry(@onNull T entry, int configChanges)154     protected abstract boolean shouldInvalidateEntry(@NonNull T entry, int configChanges);
155 
156     /**
157      * Returns the cached data for the specified theme, optionally creating a
158      * new entry if one does not already exist.
159      *
160      * @param t the theme for which to return cached data
161      * @param create {@code true} to create an entry if one does not already
162      *               exist, {@code false} otherwise
163      * @return the cached data for the theme, or {@code null} if the cache is
164      *         empty and {@code create} was {@code false}
165      */
166     @Nullable
getThemedLocked(@ullable Theme t, boolean create)167     private LongSparseArray<WeakReference<T>> getThemedLocked(@Nullable Theme t, boolean create) {
168         if (t == null) {
169             if (mNullThemedEntries == null && create) {
170                 mNullThemedEntries = new LongSparseArray<>(1);
171             }
172             return mNullThemedEntries;
173         }
174 
175         if (mThemedEntries == null) {
176             if (create) {
177                 mThemedEntries = new ArrayMap<>(1);
178             } else {
179                 return null;
180             }
181         }
182 
183         final ThemeKey key = t.getKey();
184         LongSparseArray<WeakReference<T>> cache = mThemedEntries.get(key);
185         if (cache == null && create) {
186             cache = new LongSparseArray<>(1);
187 
188             final ThemeKey keyClone = key.clone();
189             mThemedEntries.put(keyClone, cache);
190         }
191 
192         return cache;
193     }
194 
195     /**
196      * Returns the theme-agnostic cached data.
197      *
198      * @param create {@code true} to create an entry if one does not already
199      *               exist, {@code false} otherwise
200      * @return the theme-agnostic cached data, or {@code null} if the cache is
201      *         empty and {@code create} was {@code false}
202      */
203     @Nullable
getUnthemedLocked(boolean create)204     private LongSparseArray<WeakReference<T>> getUnthemedLocked(boolean create) {
205         if (mUnthemedEntries == null && create) {
206             mUnthemedEntries = new LongSparseArray<>(1);
207         }
208         return mUnthemedEntries;
209     }
210 
211     /**
212      * Prunes cache entries affected by configuration changes or where weak
213      * references have expired.
214      *
215      * @param configChanges a bitmask of configuration changes, or {@code 0} to
216      *                      simply prune missing weak references
217      * @return {@code true} if the cache is completely empty after pruning
218      */
pruneLocked(@onfig int configChanges)219     private boolean pruneLocked(@Config int configChanges) {
220         if (mThemedEntries != null) {
221             for (int i = mThemedEntries.size() - 1; i >= 0; i--) {
222                 if (pruneEntriesLocked(mThemedEntries.valueAt(i), configChanges)) {
223                     mThemedEntries.removeAt(i);
224                 }
225             }
226         }
227 
228         pruneEntriesLocked(mNullThemedEntries, configChanges);
229         pruneEntriesLocked(mUnthemedEntries, configChanges);
230 
231         return mThemedEntries == null && mNullThemedEntries == null
232                 && mUnthemedEntries == null;
233     }
234 
pruneEntriesLocked(@ullable LongSparseArray<WeakReference<T>> entries, @Config int configChanges)235     private boolean pruneEntriesLocked(@Nullable LongSparseArray<WeakReference<T>> entries,
236             @Config int configChanges) {
237         if (entries == null) {
238             return true;
239         }
240 
241         for (int i = entries.size() - 1; i >= 0; i--) {
242             final WeakReference<T> ref = entries.valueAt(i);
243             if (ref == null || pruneEntryLocked(ref.get(), configChanges)) {
244                 entries.removeAt(i);
245             }
246         }
247 
248         return entries.size() == 0;
249     }
250 
pruneEntryLocked(@ullable T entry, @Config int configChanges)251     private boolean pruneEntryLocked(@Nullable T entry, @Config int configChanges) {
252         return entry == null || (configChanges != 0
253                 && shouldInvalidateEntry(entry, configChanges));
254     }
255 
clear()256     public synchronized void clear() {
257         if (mThemedEntries != null) {
258             mThemedEntries.clear();
259         }
260 
261         if (mUnthemedEntries != null) {
262             mUnthemedEntries.clear();
263         }
264 
265         if (mNullThemedEntries != null) {
266             mNullThemedEntries.clear();
267         }
268     }
269 }
270