blob: cfa53579d010e09a5decdec1d0e29c541a21e411 [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
Steve McKayd9caa6a2016-09-15 16:36:45 -070029import com.android.documentsui.base.Shared;
30
Garfield, Tanc8099c02016-05-02 12:01:30 -070031import java.lang.annotation.Retention;
32import java.lang.annotation.RetentionPolicy;
33import java.util.Comparator;
34import java.util.HashMap;
35import 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 */
41public 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 */
57 public ThumbnailCache(int maxCacheSizeInBytes) {
58 mSizeIndex = new HashMap<>();
59 mCache = new Cache(maxCacheSizeInBytes);
Jeff Sharkey8a8fb672013-05-07 12:41:33 -070060 }
61
Garfield, Tanc8099c02016-05-02 12:01:30 -070062 /**
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 */
69 public Result getThumbnail(Uri uri, Point size) {
Garfield, Tanc8099c02016-05-02 12:01:30 -070070 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.
Garfield, Tan16502a82016-05-19 15:36:36 -070074 return Result.obtainMiss();
Garfield, Tanc8099c02016-05-02 12:01:30 -070075 }
76
77 // Look for thumbnail of the same size.
78 Pair<Uri, Point> cacheKey = sizeMap.get(size);
79 if (cacheKey != null) {
Garfield, Tan16502a82016-05-19 15:36:36 -070080 Entry entry = mCache.get(cacheKey);
81 if (entry != null) {
82 return Result.obtain(Result.CACHE_HIT_EXACT, size, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -070083 }
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) {
Garfield, Tan16502a82016-05-19 15:36:36 -070092 Entry entry = mCache.get(cacheKey);
93 if (entry != null) {
94 return Result.obtain(Result.CACHE_HIT_LARGER, otherSize, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -070095 }
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) {
Garfield, Tan16502a82016-05-19 15:36:36 -0700105 Entry entry = mCache.get(cacheKey);
106 if (entry != null) {
107 return Result.obtain(Result.CACHE_HIT_SMALLER, otherSize, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -0700108 }
109 }
110 }
111
112 // Cache miss.
Garfield, Tan16502a82016-05-19 15:36:36 -0700113 return Result.obtainMiss();
Garfield, Tanc8099c02016-05-02 12:01:30 -0700114 }
115
Garfield, Taneba5bb92016-07-22 10:30:49 -0700116 /**
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 */
Garfield, Tan16502a82016-05-19 15:36:36 -0700123 public void putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified) {
Garfield, Tanc8099c02016-05-02 12:01:30 -0700124 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
Garfield, Tan16502a82016-05-19 15:36:36 -0700135 Entry entry = new Entry(thumbnail, lastModified);
136 mCache.put(cacheKey, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -0700137 synchronized (sizeMap) {
138 sizeMap.put(size, cacheKey);
139 }
140 }
141
Garfield, Taneba5bb92016-07-22 10:30:49 -0700142 /**
143 * Removes all thumbnail cache associated to the given uri.
144 * @param uri the uri which thumbnail cache to remove
145 */
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
Garfield, Tan16502a82016-05-19 15:36:36 -0700162 private void removeKey(Uri uri, Point size) {
163 TreeMap<Point, Pair<Uri, Point>> sizeMap;
164 synchronized (mSizeIndex) {
165 sizeMap = mSizeIndex.get(uri);
166 }
167
Garfield, Taneba5bb92016-07-22 10:30:49 -0700168 assert(sizeMap != null);
Garfield, Tan16502a82016-05-19 15:36:36 -0700169 synchronized (sizeMap) {
170 sizeMap.remove(size);
171 }
172 }
173
Garfield, Tanc8099c02016-05-02 12:01:30 -0700174 public void onTrimMemory(int level) {
175 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
Garfield, Tanc8099c02016-05-02 12:01:30 -0700176 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 {}
Garfield, Tanc8099c02016-05-02 12:01:30 -0700190 /**
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;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700212 private @Nullable Bitmap mThumbnail;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700213 private @Nullable Point mSize;
Garfield, Tan16502a82016-05-19 15:36:36 -0700214 private long mLastModified;
215
216 private static Result obtainMiss() {
217 return obtain(CACHE_MISS, null, null, 0);
218 }
219
220 private static Result obtain(@Status int status, Point size, Entry entry) {
221 return obtain(status, entry.mThumbnail, size, entry.mLastModified);
222 }
Garfield, Tanc8099c02016-05-02 12:01:30 -0700223
224 private static Result obtain(@Status int status, @Nullable Bitmap thumbnail,
Garfield, Tan16502a82016-05-19 15:36:36 -0700225 @Nullable Point size, long lastModified) {
Garfield, Tan20562052016-06-10 11:23:40 -0700226 Shared.checkMainLoop();
Garfield, Tanc2923782016-06-07 15:09:51 -0700227
Garfield, Tanc8099c02016-05-02 12:01:30 -0700228 Result instance = sPool.acquire();
229 instance = (instance != null ? instance : new Result());
230
231 instance.mStatus = status;
232 instance.mThumbnail = thumbnail;
233 instance.mSize = size;
Garfield, Tan16502a82016-05-19 15:36:36 -0700234 instance.mLastModified = lastModified;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700235
236 return instance;
237 }
238
Garfield, Tan16502a82016-05-19 15:36:36 -0700239 private Result() {}
Garfield, Tanc8099c02016-05-02 12:01:30 -0700240
241 public void recycle() {
Garfield, Tan20562052016-06-10 11:23:40 -0700242 Shared.checkMainLoop();
Garfield, Tanc2923782016-06-07 15:09:51 -0700243
Garfield, Tanc8099c02016-05-02 12:01:30 -0700244 mStatus = -1;
245 mThumbnail = null;
246 mSize = null;
Garfield, Tan16502a82016-05-19 15:36:36 -0700247 mLastModified = -1;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700248
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
258 public @Status int getStatus() {
259 return mStatus;
260 }
261
262 public @Nullable Bitmap getThumbnail() {
263 return mThumbnail;
264 }
265
266 public @Nullable Point getSize() {
267 return mSize;
268 }
269
Garfield, Tan16502a82016-05-19 15:36:36 -0700270 public long getLastModified() {
271 return mLastModified;
272 }
273
Garfield, Tanc8099c02016-05-02 12:01:30 -0700274 public boolean isHit() {
275 return (mStatus != CACHE_MISS);
276 }
277
278 public boolean isExactHit() {
279 return (mStatus == CACHE_HIT_EXACT);
280 }
281 }
282
Garfield, Tan16502a82016-05-19 15:36:36 -0700283 private static final class Entry {
284 private final Bitmap mThumbnail;
285 private final long mLastModified;
286
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
Garfield, Tanc8099c02016-05-02 12:01:30 -0700295 private Cache(int maxSizeBytes) {
296 super(maxSizeBytes);
297 }
298
299 @Override
Garfield, Tan16502a82016-05-19 15:36:36 -0700300 protected int sizeOf(Pair<Uri, Point> key, Entry value) {
301 return value.mThumbnail.getByteCount();
302 }
303
304 @Override
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 }
Garfield, Tanc8099c02016-05-02 12:01:30 -0700310 }
311 }
312
313 private static final class SizeComparator implements Comparator<Point> {
314 @Override
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 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700319 }
320}