1 /*
2  * Copyright (C) 2011 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 com.android.dialer.util;
18 
19 import android.util.LruCache;
20 import java.util.concurrent.atomic.AtomicInteger;
21 import javax.annotation.concurrent.Immutable;
22 import javax.annotation.concurrent.ThreadSafe;
23 
24 /**
25  * An LRU cache in which all items can be marked as expired at a given time and it is possible to
26  * query whether a particular cached value is expired or not.
27  *
28  * <p>A typical use case for this is caching of values which are expensive to compute but which are
29  * still useful when out of date.
30  *
31  * <p>Consider a cache for contact information:
32  *
33  * <pre>{@code
34  * private ExpirableCache<String, Contact> mContactCache;
35  * }</pre>
36  *
37  * which stores the contact information for a given phone number.
38  *
39  * <p>When we need to store contact information for a given phone number, we can look up the info in
40  * the cache:
41  *
42  * <pre>{@code
43  * CachedValue<Contact> cachedContact = mContactCache.getCachedValue(phoneNumber);
44  * }</pre>
45  *
46  * We might also want to fetch the contact information again if the item is expired.
47  *
48  * <pre>
49  *     if (cachedContact.isExpired()) {
50  *         fetchContactForNumber(phoneNumber,
51  *                 new FetchListener() {
52  *                     &#64;Override
53  *                     public void onFetched(Contact contact) {
54  *                         mContactCache.put(phoneNumber, contact);
55  *                     }
56  *                 });
57  *     }</pre>
58  *
59  * and insert it back into the cache when the fetch completes.
60  *
61  * <p>At a certain point we want to expire the content of the cache because we know the content may
62  * no longer be up-to-date, for instance, when resuming the activity this is shown into:
63  *
64  * <pre>
65  *     &#64;Override
66  *     protected onResume() {
67  *         // We were paused for some time, the cached value might no longer be up to date.
68  *         mContactCache.expireAll();
69  *         super.onResume();
70  *     }
71  * </pre>
72  *
73  * The values will be still available from the cache, but they will be expired.
74  *
75  * <p>If interested only in the value itself, not whether it is expired or not, one should use the
76  * {@link #getPossiblyExpired(Object)} method. If interested only in non-expired values, one should
77  * use the {@link #get(Object)} method instead.
78  *
79  * <p>This class wraps around an {@link LruCache} instance: it follows the {@link LruCache} behavior
80  * for evicting items when the cache is full. It is possible to supply your own subclass of LruCache
81  * by using the {@link #create(LruCache)} method, which can define a custom expiration policy. Since
82  * the underlying cache maps keys to cached values it can determine which items are expired and
83  * which are not, allowing for an implementation that evicts expired items before non expired ones.
84  *
85  * <p>This class is thread-safe.
86  *
87  * @param <K> the type of the keys
88  * @param <V> the type of the values
89  */
90 @ThreadSafe
91 public class ExpirableCache<K, V> {
92 
93   /**
94    * The current generation of items added to the cache.
95    *
96    * <p>Items in the cache can belong to a previous generation, but in that case they would be
97    * expired.
98    *
99    * @see ExpirableCache.CachedValue#isExpired()
100    */
101   private final AtomicInteger mGeneration;
102   /** The underlying cache used to stored the cached values. */
103   private LruCache<K, CachedValue<V>> mCache;
104 
ExpirableCache(LruCache<K, CachedValue<V>> cache)105   private ExpirableCache(LruCache<K, CachedValue<V>> cache) {
106     mCache = cache;
107     mGeneration = new AtomicInteger(0);
108   }
109 
110   /**
111    * Creates a new {@link ExpirableCache} that wraps the given {@link LruCache}.
112    *
113    * <p>The created cache takes ownership of the cache passed in as an argument.
114    *
115    * @param <K> the type of the keys
116    * @param <V> the type of the values
117    * @param cache the cache to store the value in
118    * @return the newly created expirable cache
119    * @throws IllegalArgumentException if the cache is not empty
120    */
create(LruCache<K, CachedValue<V>> cache)121   public static <K, V> ExpirableCache<K, V> create(LruCache<K, CachedValue<V>> cache) {
122     return new ExpirableCache<K, V>(cache);
123   }
124 
125   /**
126    * Creates a new {@link ExpirableCache} with the given maximum size.
127    *
128    * @param <K> the type of the keys
129    * @param <V> the type of the values
130    * @return the newly created expirable cache
131    */
create(int maxSize)132   public static <K, V> ExpirableCache<K, V> create(int maxSize) {
133     return create(new LruCache<K, CachedValue<V>>(maxSize));
134   }
135 
136   /**
137    * Returns the cached value for the given key, or null if no value exists.
138    *
139    * <p>The cached value gives access both to the value associated with the key and whether it is
140    * expired or not.
141    *
142    * <p>If not interested in whether the value is expired, use {@link #getPossiblyExpired(Object)}
143    * instead.
144    *
145    * <p>If only wants values that are not expired, use {@link #get(Object)} instead.
146    *
147    * @param key the key to look up
148    */
getCachedValue(K key)149   public CachedValue<V> getCachedValue(K key) {
150     return mCache.get(key);
151   }
152 
153   /**
154    * Returns the value for the given key, or null if no value exists.
155    *
156    * <p>When using this method, it is not possible to determine whether the value is expired or not.
157    * Use {@link #getCachedValue(Object)} to achieve that instead. However, if using {@link
158    * #getCachedValue(Object)} to determine if an item is expired, one should use the item within the
159    * {@link CachedValue} and not call {@link #getPossiblyExpired(Object)} to get the value
160    * afterwards, since that is not guaranteed to return the same value or that the newly returned
161    * value is in the same state.
162    *
163    * @param key the key to look up
164    */
getPossiblyExpired(K key)165   public V getPossiblyExpired(K key) {
166     CachedValue<V> cachedValue = getCachedValue(key);
167     return cachedValue == null ? null : cachedValue.getValue();
168   }
169 
170   /**
171    * Returns the value for the given key only if it is not expired, or null if no value exists or is
172    * expired.
173    *
174    * <p>This method will return null if either there is no value associated with this key or if the
175    * associated value is expired.
176    *
177    * @param key the key to look up
178    */
get(K key)179   public V get(K key) {
180     CachedValue<V> cachedValue = getCachedValue(key);
181     return cachedValue == null || cachedValue.isExpired() ? null : cachedValue.getValue();
182   }
183 
184   /**
185    * Puts an item in the cache.
186    *
187    * <p>Newly added item will not be expired until {@link #expireAll()} is next called.
188    *
189    * @param key the key to look up
190    * @param value the value to associate with the key
191    */
put(K key, V value)192   public void put(K key, V value) {
193     mCache.put(key, newCachedValue(value));
194   }
195 
196   /**
197    * Mark all items currently in the cache as expired.
198    *
199    * <p>Newly added items after this call will be marked as not expired.
200    *
201    * <p>Expiring the items in the cache does not imply they will be evicted.
202    */
expireAll()203   public void expireAll() {
204     mGeneration.incrementAndGet();
205   }
206 
207   /**
208    * Creates a new {@link CachedValue} instance to be stored in this cache.
209    *
210    * <p>Implementation of {@link LruCache#create(K)} can use this method to create a new entry.
211    */
newCachedValue(V value)212   public CachedValue<V> newCachedValue(V value) {
213     return new GenerationalCachedValue<V>(value, mGeneration);
214   }
215 
216   /**
217    * A cached value stored inside the cache.
218    *
219    * <p>It provides access to the value stored in the cache but also allows to check whether the
220    * value is expired.
221    *
222    * @param <V> the type of value stored in the cache
223    */
224   public interface CachedValue<V> {
225 
226     /** Returns the value stored in the cache for a given key. */
getValue()227     V getValue();
228 
229     /**
230      * Checks whether the value, while still being present in the cache, is expired.
231      *
232      * @return true if the value is expired
233      */
isExpired()234     boolean isExpired();
235   }
236 
237   /** Cached values storing the generation at which they were added. */
238   @Immutable
239   private static class GenerationalCachedValue<V> implements ExpirableCache.CachedValue<V> {
240 
241     /** The value stored in the cache. */
242     public final V mValue;
243     /** The generation at which the value was added to the cache. */
244     private final int mGeneration;
245     /** The atomic integer storing the current generation of the cache it belongs to. */
246     private final AtomicInteger mCacheGeneration;
247 
248     /**
249      * @param cacheGeneration the atomic integer storing the generation of the cache in which this
250      *     value will be stored
251      */
GenerationalCachedValue(V value, AtomicInteger cacheGeneration)252     public GenerationalCachedValue(V value, AtomicInteger cacheGeneration) {
253       mValue = value;
254       mCacheGeneration = cacheGeneration;
255       // Snapshot the current generation.
256       mGeneration = mCacheGeneration.get();
257     }
258 
259     @Override
getValue()260     public V getValue() {
261       return mValue;
262     }
263 
264     @Override
isExpired()265     public boolean isExpired() {
266       return mGeneration != mCacheGeneration.get();
267     }
268   }
269 }
270