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