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