blob: 639d4fb4feb317aa35e34d63e2c7c1ffa2720655 [file] [log] [blame]
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07001/*
Garfield, Tanc8099c02016-05-02 12:01:30 -07002 * Copyright (C) 2016 The Android Open Source Project
Jeff Sharkey8a8fb672013-05-07 12:41:33 -07003 *
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
17package com.android.documentsui;
18
Garfield, Tanc8099c02016-05-02 12:01:30 -070019import android.annotation.IntDef;
20import android.annotation.Nullable;
21import android.content.ComponentCallbacks2;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070022import android.graphics.Bitmap;
Garfield, Tanc8099c02016-05-02 12:01:30 -070023import android.graphics.Point;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070024import android.net.Uri;
25import android.util.LruCache;
Garfield, Tanc8099c02016-05-02 12:01:30 -070026import android.util.Pair;
27import android.util.Pools;
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070028
Garfield, Tanc8099c02016-05-02 12:01:30 -070029import java.lang.annotation.Retention;
30import java.lang.annotation.RetentionPolicy;
31import java.util.Comparator;
32import java.util.HashMap;
33import java.util.TreeMap;
34
35/**
36 * An LRU cache that supports finding the thumbnail of the requested uri with a different size than
37 * the requested one.
38 */
39public class ThumbnailCache {
40
41 private static final SizeComparator SIZE_COMPARATOR = new SizeComparator();
42
43 /**
44 * A 2-dimensional index into {@link #mCache} entries. Pair<Uri, Point> is the key to
45 * {@link #mCache}. TreeMap is used to search the closest size to a given size and a given uri.
46 */
47 private final HashMap<Uri, TreeMap<Point, Pair<Uri, Point>>> mSizeIndex;
48 private final Cache mCache;
49
50 /**
51 * Creates a thumbnail LRU cache.
52 *
53 * @param maxCacheSizeInBytes the maximum size of thumbnails in bytes this cache can hold.
54 */
55 public ThumbnailCache(int maxCacheSizeInBytes) {
56 mSizeIndex = new HashMap<>();
57 mCache = new Cache(maxCacheSizeInBytes);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070058 }
59
Garfield, Tanc8099c02016-05-02 12:01:30 -070060 /**
61 * Obtains thumbnail given a uri and a size.
62 *
63 * @param uri the uri of the thumbnail in need
64 * @param size the desired size of the thumbnail
65 * @return the thumbnail result
66 */
67 public Result getThumbnail(Uri uri, Point size) {
Garfield, Tanc8099c02016-05-02 12:01:30 -070068 TreeMap<Point, Pair<Uri, Point>> sizeMap;
69 sizeMap = mSizeIndex.get(uri);
70 if (sizeMap == null || sizeMap.isEmpty()) {
71 // There is not any thumbnail for this uri.
Garfield, Tan16502a82016-05-19 15:36:36 -070072 return Result.obtainMiss();
Garfield, Tanc8099c02016-05-02 12:01:30 -070073 }
74
75 // Look for thumbnail of the same size.
76 Pair<Uri, Point> cacheKey = sizeMap.get(size);
77 if (cacheKey != null) {
Garfield, Tan16502a82016-05-19 15:36:36 -070078 Entry entry = mCache.get(cacheKey);
79 if (entry != null) {
80 return Result.obtain(Result.CACHE_HIT_EXACT, size, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -070081 }
82 }
83
84 // Look for thumbnail of bigger sizes.
85 Point otherSize = sizeMap.higherKey(size);
86 if (otherSize != null) {
87 cacheKey = sizeMap.get(otherSize);
88
89 if (cacheKey != null) {
Garfield, Tan16502a82016-05-19 15:36:36 -070090 Entry entry = mCache.get(cacheKey);
91 if (entry != null) {
92 return Result.obtain(Result.CACHE_HIT_LARGER, otherSize, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -070093 }
94 }
95 }
96
97 // Look for thumbnail of smaller sizes.
98 otherSize = sizeMap.lowerKey(size);
99 if (otherSize != null) {
100 cacheKey = sizeMap.get(otherSize);
101
102 if (cacheKey != null) {
Garfield, Tan16502a82016-05-19 15:36:36 -0700103 Entry entry = mCache.get(cacheKey);
104 if (entry != null) {
105 return Result.obtain(Result.CACHE_HIT_SMALLER, otherSize, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -0700106 }
107 }
108 }
109
110 // Cache miss.
Garfield, Tan16502a82016-05-19 15:36:36 -0700111 return Result.obtainMiss();
Garfield, Tanc8099c02016-05-02 12:01:30 -0700112 }
113
Garfield, Taneba5bb92016-07-22 10:30:49 -0700114 /**
115 * Puts a thumbnail for the given uri and size in to the cache.
116 * @param uri the uri of the thumbnail
117 * @param size the size of the thumbnail
118 * @param thumbnail the thumbnail to put in cache
119 * @param lastModified last modified value of the thumbnail to track its validity
120 */
Garfield, Tan16502a82016-05-19 15:36:36 -0700121 public void putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified) {
Garfield, Tanc8099c02016-05-02 12:01:30 -0700122 Pair<Uri, Point> cacheKey = Pair.create(uri, size);
123
124 TreeMap<Point, Pair<Uri, Point>> sizeMap;
125 synchronized (mSizeIndex) {
126 sizeMap = mSizeIndex.get(uri);
127 if (sizeMap == null) {
128 sizeMap = new TreeMap<>(SIZE_COMPARATOR);
129 mSizeIndex.put(uri, sizeMap);
130 }
131 }
132
Garfield, Tan16502a82016-05-19 15:36:36 -0700133 Entry entry = new Entry(thumbnail, lastModified);
134 mCache.put(cacheKey, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -0700135 synchronized (sizeMap) {
136 sizeMap.put(size, cacheKey);
137 }
138 }
139
Garfield, Taneba5bb92016-07-22 10:30:49 -0700140 /**
141 * Removes all thumbnail cache associated to the given uri.
142 * @param uri the uri which thumbnail cache to remove
143 */
144 public void removeUri(Uri uri) {
145 TreeMap<Point, Pair<Uri, Point>> sizeMap;
146 synchronized (mSizeIndex) {
147 sizeMap = mSizeIndex.get(uri);
148 }
149
150 if (sizeMap != null) {
151 // Create an array to hold all values to avoid ConcurrentModificationException because
152 // removeKey() will be called by LruCache but we can't modify the map while we're
153 // iterating over the collection of values.
154 for (Pair<Uri, Point> index : sizeMap.values().toArray(new Pair[0])) {
155 mCache.remove(index);
156 }
157 }
158 }
159
Garfield, Tan16502a82016-05-19 15:36:36 -0700160 private void removeKey(Uri uri, Point size) {
161 TreeMap<Point, Pair<Uri, Point>> sizeMap;
162 synchronized (mSizeIndex) {
163 sizeMap = mSizeIndex.get(uri);
164 }
165
Garfield, Taneba5bb92016-07-22 10:30:49 -0700166 assert(sizeMap != null);
Garfield, Tan16502a82016-05-19 15:36:36 -0700167 synchronized (sizeMap) {
168 sizeMap.remove(size);
169 }
170 }
171
Garfield, Tanc8099c02016-05-02 12:01:30 -0700172 public void onTrimMemory(int level) {
173 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
Garfield, Tanc8099c02016-05-02 12:01:30 -0700174 mCache.evictAll();
175 } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
176 mCache.trimToSize(mCache.size() / 2);
177 }
178 }
179
180 /**
181 * A class that holds thumbnail and cache status.
182 */
183 public static final class Result {
184
185 @Retention(RetentionPolicy.SOURCE)
186 @IntDef({CACHE_MISS, CACHE_HIT_EXACT, CACHE_HIT_SMALLER, CACHE_HIT_LARGER})
187 @interface Status {}
Garfield, Tanc8099c02016-05-02 12:01:30 -0700188 /**
189 * Indicates there is no thumbnail for the requested uri. The thumbnail will be null.
190 */
191 public static final int CACHE_MISS = 0;
192 /**
193 * Indicates the thumbnail matches the requested size and requested uri.
194 */
195 public static final int CACHE_HIT_EXACT = 1;
196 /**
197 * Indicates the thumbnail is in a smaller size than the requested one from the requested
198 * uri.
199 */
200 public static final int CACHE_HIT_SMALLER = 2;
201 /**
202 * Indicates the thumbnail is in a larger size than the requested one from the requested
203 * uri.
204 */
205 public static final int CACHE_HIT_LARGER = 3;
206
207 private static final Pools.SimplePool<Result> sPool = new Pools.SimplePool<>(1);
208
209 private @Status int mStatus;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700210 private @Nullable Bitmap mThumbnail;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700211 private @Nullable Point mSize;
Garfield, Tan16502a82016-05-19 15:36:36 -0700212 private long mLastModified;
213
214 private static Result obtainMiss() {
215 return obtain(CACHE_MISS, null, null, 0);
216 }
217
218 private static Result obtain(@Status int status, Point size, Entry entry) {
219 return obtain(status, entry.mThumbnail, size, entry.mLastModified);
220 }
Garfield, Tanc8099c02016-05-02 12:01:30 -0700221
222 private static Result obtain(@Status int status, @Nullable Bitmap thumbnail,
Garfield, Tan16502a82016-05-19 15:36:36 -0700223 @Nullable Point size, long lastModified) {
Garfield, Tan20562052016-06-10 11:23:40 -0700224 Shared.checkMainLoop();
Garfield, Tanc2923782016-06-07 15:09:51 -0700225
Garfield, Tanc8099c02016-05-02 12:01:30 -0700226 Result instance = sPool.acquire();
227 instance = (instance != null ? instance : new Result());
228
229 instance.mStatus = status;
230 instance.mThumbnail = thumbnail;
231 instance.mSize = size;
Garfield, Tan16502a82016-05-19 15:36:36 -0700232 instance.mLastModified = lastModified;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700233
234 return instance;
235 }
236
Garfield, Tan16502a82016-05-19 15:36:36 -0700237 private Result() {}
Garfield, Tanc8099c02016-05-02 12:01:30 -0700238
239 public void recycle() {
Garfield, Tan20562052016-06-10 11:23:40 -0700240 Shared.checkMainLoop();
Garfield, Tanc2923782016-06-07 15:09:51 -0700241
Garfield, Tanc8099c02016-05-02 12:01:30 -0700242 mStatus = -1;
243 mThumbnail = null;
244 mSize = null;
Garfield, Tan16502a82016-05-19 15:36:36 -0700245 mLastModified = -1;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700246
247 boolean released = sPool.release(this);
248 // This assert is used to guarantee we won't generate too many instances that can't be
249 // held in the pool, which indicates our pool size is too small.
250 //
251 // Right now one instance is enough because we expect all instances are only used in
252 // main thread.
253 assert (released);
254 }
255
256 public @Status int getStatus() {
257 return mStatus;
258 }
259
260 public @Nullable Bitmap getThumbnail() {
261 return mThumbnail;
262 }
263
264 public @Nullable Point getSize() {
265 return mSize;
266 }
267
Garfield, Tan16502a82016-05-19 15:36:36 -0700268 public long getLastModified() {
269 return mLastModified;
270 }
271
Garfield, Tanc8099c02016-05-02 12:01:30 -0700272 public boolean isHit() {
273 return (mStatus != CACHE_MISS);
274 }
275
276 public boolean isExactHit() {
277 return (mStatus == CACHE_HIT_EXACT);
278 }
279 }
280
Garfield, Tan16502a82016-05-19 15:36:36 -0700281 private static final class Entry {
282 private final Bitmap mThumbnail;
283 private final long mLastModified;
284
285 private Entry(Bitmap thumbnail, long lastModified) {
286 mThumbnail = thumbnail;
287 mLastModified = lastModified;
288 }
289 }
290
291 private final class Cache extends LruCache<Pair<Uri, Point>, Entry> {
292
Garfield, Tanc8099c02016-05-02 12:01:30 -0700293 private Cache(int maxSizeBytes) {
294 super(maxSizeBytes);
295 }
296
297 @Override
Garfield, Tan16502a82016-05-19 15:36:36 -0700298 protected int sizeOf(Pair<Uri, Point> key, Entry value) {
299 return value.mThumbnail.getByteCount();
300 }
301
302 @Override
303 protected void entryRemoved(
304 boolean evicted, Pair<Uri, Point> key, Entry oldValue, Entry newValue) {
305 if (newValue == null) {
306 removeKey(key.first, key.second);
307 }
Garfield, Tanc8099c02016-05-02 12:01:30 -0700308 }
309 }
310
311 private static final class SizeComparator implements Comparator<Point> {
312 @Override
313 public int compare(Point size0, Point size1) {
314 // Assume all sizes are roughly square, so we only compare them in one dimension.
315 return size0.x - size1.x;
316 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700317 }
318}