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