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 
17 package com.android.documentsui;
18 
19 import android.annotation.IntDef;
20 import android.annotation.Nullable;
21 import android.content.ComponentCallbacks2;
22 import android.graphics.Bitmap;
23 import android.graphics.Point;
24 import android.net.Uri;
25 import android.util.LruCache;
26 import android.util.Pair;
27 import android.util.Pools;
28 
29 import com.android.documentsui.base.Shared;
30 
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.RetentionPolicy;
33 import java.util.Comparator;
34 import java.util.HashMap;
35 import java.util.TreeMap;
36 
37 /**
38  * An LRU cache that supports finding the thumbnail of the requested uri with a different size than
39  * the requested one.
40  */
41 public class ThumbnailCache {
42 
43     private static final SizeComparator SIZE_COMPARATOR = new SizeComparator();
44 
45     /**
46      * A 2-dimensional index into {@link #mCache} entries. Pair<Uri, Point> is the key to
47      * {@link #mCache}. TreeMap is used to search the closest size to a given size and a given uri.
48      */
49     private final HashMap<Uri, TreeMap<Point, Pair<Uri, Point>>> mSizeIndex;
50     private final Cache mCache;
51 
52     /**
53      * Creates a thumbnail LRU cache.
54      *
55      * @param maxCacheSizeInBytes the maximum size of thumbnails in bytes this cache can hold.
56      */
ThumbnailCache(int maxCacheSizeInBytes)57     public ThumbnailCache(int maxCacheSizeInBytes) {
58         mSizeIndex = new HashMap<>();
59         mCache = new Cache(maxCacheSizeInBytes);
60     }
61 
62     /**
63      * Obtains thumbnail given a uri and a size.
64      *
65      * @param uri the uri of the thumbnail in need
66      * @param size the desired size of the thumbnail
67      * @return the thumbnail result
68      */
getThumbnail(Uri uri, Point size)69     public Result getThumbnail(Uri uri, Point size) {
70         TreeMap<Point, Pair<Uri, Point>> sizeMap;
71         sizeMap = mSizeIndex.get(uri);
72         if (sizeMap == null || sizeMap.isEmpty()) {
73             // There is not any thumbnail for this uri.
74             return Result.obtainMiss();
75         }
76 
77         // Look for thumbnail of the same size.
78         Pair<Uri, Point> cacheKey = sizeMap.get(size);
79         if (cacheKey != null) {
80             Entry entry = mCache.get(cacheKey);
81             if (entry != null) {
82                 return Result.obtain(Result.CACHE_HIT_EXACT, size, entry);
83             }
84         }
85 
86         // Look for thumbnail of bigger sizes.
87         Point otherSize = sizeMap.higherKey(size);
88         if (otherSize != null) {
89             cacheKey = sizeMap.get(otherSize);
90 
91             if (cacheKey != null) {
92                 Entry entry = mCache.get(cacheKey);
93                 if (entry != null) {
94                     return Result.obtain(Result.CACHE_HIT_LARGER, otherSize, entry);
95                 }
96             }
97         }
98 
99         // Look for thumbnail of smaller sizes.
100         otherSize = sizeMap.lowerKey(size);
101         if (otherSize != null) {
102             cacheKey = sizeMap.get(otherSize);
103 
104             if (cacheKey != null) {
105                 Entry entry = mCache.get(cacheKey);
106                 if (entry != null) {
107                     return Result.obtain(Result.CACHE_HIT_SMALLER, otherSize, entry);
108                 }
109             }
110         }
111 
112         // Cache miss.
113         return Result.obtainMiss();
114     }
115 
116     /**
117      * Puts a thumbnail for the given uri and size in to the cache.
118      * @param uri the uri of the thumbnail
119      * @param size the size of the thumbnail
120      * @param thumbnail the thumbnail to put in cache
121      * @param lastModified last modified value of the thumbnail to track its validity
122      */
putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified)123     public void putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified) {
124         Pair<Uri, Point> cacheKey = Pair.create(uri, size);
125 
126         TreeMap<Point, Pair<Uri, Point>> sizeMap;
127         synchronized (mSizeIndex) {
128             sizeMap = mSizeIndex.get(uri);
129             if (sizeMap == null) {
130                 sizeMap = new TreeMap<>(SIZE_COMPARATOR);
131                 mSizeIndex.put(uri, sizeMap);
132             }
133         }
134 
135         Entry entry = new Entry(thumbnail, lastModified);
136         mCache.put(cacheKey, entry);
137         synchronized (sizeMap) {
138             sizeMap.put(size, cacheKey);
139         }
140     }
141 
142     /**
143      * Removes all thumbnail cache associated to the given uri.
144      * @param uri the uri which thumbnail cache to remove
145      */
removeUri(Uri uri)146     public void removeUri(Uri uri) {
147         TreeMap<Point, Pair<Uri, Point>> sizeMap;
148         synchronized (mSizeIndex) {
149             sizeMap = mSizeIndex.get(uri);
150         }
151 
152         if (sizeMap != null) {
153             // Create an array to hold all values to avoid ConcurrentModificationException because
154             // removeKey() will be called by LruCache but we can't modify the map while we're
155             // iterating over the collection of values.
156             for (Pair<Uri, Point> index : sizeMap.values().toArray(new Pair[0])) {
157                 mCache.remove(index);
158             }
159         }
160     }
161 
removeKey(Uri uri, Point size)162     private void removeKey(Uri uri, Point size) {
163         TreeMap<Point, Pair<Uri, Point>> sizeMap;
164         synchronized (mSizeIndex) {
165             sizeMap = mSizeIndex.get(uri);
166         }
167 
168         assert(sizeMap != null);
169         synchronized (sizeMap) {
170             sizeMap.remove(size);
171         }
172     }
173 
onTrimMemory(int level)174     public void onTrimMemory(int level) {
175         if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
176             mCache.evictAll();
177         } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
178             mCache.trimToSize(mCache.size() / 2);
179         }
180     }
181 
182     /**
183      * A class that holds thumbnail and cache status.
184      */
185     public static final class Result {
186 
187         @Retention(RetentionPolicy.SOURCE)
188         @IntDef({CACHE_MISS, CACHE_HIT_EXACT, CACHE_HIT_SMALLER, CACHE_HIT_LARGER})
189         @interface Status {}
190         /**
191          * Indicates there is no thumbnail for the requested uri. The thumbnail will be null.
192          */
193         public static final int CACHE_MISS = 0;
194         /**
195          * Indicates the thumbnail matches the requested size and requested uri.
196          */
197         public static final int CACHE_HIT_EXACT = 1;
198         /**
199          * Indicates the thumbnail is in a smaller size than the requested one from the requested
200          * uri.
201          */
202         public static final int CACHE_HIT_SMALLER = 2;
203         /**
204          * Indicates the thumbnail is in a larger size than the requested one from the requested
205          * uri.
206          */
207         public static final int CACHE_HIT_LARGER = 3;
208 
209         private static final Pools.SimplePool<Result> sPool = new Pools.SimplePool<>(1);
210 
211         private @Status int mStatus;
212         private @Nullable Bitmap mThumbnail;
213         private @Nullable Point mSize;
214         private long mLastModified;
215 
obtainMiss()216         private static Result obtainMiss() {
217             return obtain(CACHE_MISS, null, null, 0);
218         }
219 
obtain(@tatus int status, Point size, Entry entry)220         private static Result obtain(@Status int status, Point size, Entry entry) {
221             return obtain(status, entry.mThumbnail, size, entry.mLastModified);
222         }
223 
obtain(@tatus int status, @Nullable Bitmap thumbnail, @Nullable Point size, long lastModified)224         private static Result obtain(@Status int status, @Nullable Bitmap thumbnail,
225                 @Nullable Point size, long lastModified) {
226             Shared.checkMainLoop();
227 
228             Result instance = sPool.acquire();
229             instance = (instance != null ? instance : new Result());
230 
231             instance.mStatus = status;
232             instance.mThumbnail = thumbnail;
233             instance.mSize = size;
234             instance.mLastModified = lastModified;
235 
236             return instance;
237         }
238 
Result()239         private Result() {}
240 
recycle()241         public void recycle() {
242             Shared.checkMainLoop();
243 
244             mStatus = -1;
245             mThumbnail = null;
246             mSize = null;
247             mLastModified = -1;
248 
249             boolean released = sPool.release(this);
250             // This assert is used to guarantee we won't generate too many instances that can't be
251             // held in the pool, which indicates our pool size is too small.
252             //
253             // Right now one instance is enough because we expect all instances are only used in
254             // main thread.
255             assert (released);
256         }
257 
getStatus()258         public @Status int getStatus() {
259             return mStatus;
260         }
261 
getThumbnail()262         public @Nullable Bitmap getThumbnail() {
263             return mThumbnail;
264         }
265 
getSize()266         public @Nullable Point getSize() {
267             return mSize;
268         }
269 
getLastModified()270         public long getLastModified() {
271             return mLastModified;
272         }
273 
isHit()274         public boolean isHit() {
275             return (mStatus != CACHE_MISS);
276         }
277 
isExactHit()278         public boolean isExactHit() {
279             return (mStatus == CACHE_HIT_EXACT);
280         }
281     }
282 
283     private static final class Entry {
284         private final Bitmap mThumbnail;
285         private final long mLastModified;
286 
Entry(Bitmap thumbnail, long lastModified)287         private Entry(Bitmap thumbnail, long lastModified) {
288             mThumbnail = thumbnail;
289             mLastModified = lastModified;
290         }
291     }
292 
293     private final class Cache extends LruCache<Pair<Uri, Point>, Entry> {
294 
Cache(int maxSizeBytes)295         private Cache(int maxSizeBytes) {
296             super(maxSizeBytes);
297         }
298 
299         @Override
sizeOf(Pair<Uri, Point> key, Entry value)300         protected int sizeOf(Pair<Uri, Point> key, Entry value) {
301             return value.mThumbnail.getByteCount();
302         }
303 
304         @Override
entryRemoved( boolean evicted, Pair<Uri, Point> key, Entry oldValue, Entry newValue)305         protected void entryRemoved(
306                 boolean evicted, Pair<Uri, Point> key, Entry oldValue, Entry newValue) {
307             if (newValue == null) {
308                 removeKey(key.first, key.second);
309             }
310         }
311     }
312 
313     private static final class SizeComparator implements Comparator<Point> {
314         @Override
compare(Point size0, Point size1)315         public int compare(Point size0, Point size1) {
316             // Assume all sizes are roughly square, so we only compare them in one dimension.
317             return size0.x - size1.x;
318         }
319     }
320 }
321