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