blob: 53f5092e93b7848a356e1fb9caf050df6a154197 [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, Tan16502a82016-05-19 15:36:36 -0700114 public void putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified) {
Garfield, Tanc8099c02016-05-02 12:01:30 -0700115 Pair<Uri, Point> cacheKey = Pair.create(uri, size);
116
117 TreeMap<Point, Pair<Uri, Point>> sizeMap;
118 synchronized (mSizeIndex) {
119 sizeMap = mSizeIndex.get(uri);
120 if (sizeMap == null) {
121 sizeMap = new TreeMap<>(SIZE_COMPARATOR);
122 mSizeIndex.put(uri, sizeMap);
123 }
124 }
125
Garfield, Tan16502a82016-05-19 15:36:36 -0700126 Entry entry = new Entry(thumbnail, lastModified);
127 mCache.put(cacheKey, entry);
Garfield, Tanc8099c02016-05-02 12:01:30 -0700128 synchronized (sizeMap) {
129 sizeMap.put(size, cacheKey);
130 }
131 }
132
Garfield, Tan16502a82016-05-19 15:36:36 -0700133 private void removeKey(Uri uri, Point size) {
134 TreeMap<Point, Pair<Uri, Point>> sizeMap;
135 synchronized (mSizeIndex) {
136 sizeMap = mSizeIndex.get(uri);
137 }
138
139 // LruCache tells us to remove a key, which should exist, so sizeMap can't be null.
140 assert (sizeMap != null);
141 synchronized (sizeMap) {
142 sizeMap.remove(size);
143 }
144 }
145
Garfield, Tanc8099c02016-05-02 12:01:30 -0700146 public void onTrimMemory(int level) {
147 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
Garfield, Tanc8099c02016-05-02 12:01:30 -0700148 mCache.evictAll();
149 } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
150 mCache.trimToSize(mCache.size() / 2);
151 }
152 }
153
154 /**
155 * A class that holds thumbnail and cache status.
156 */
157 public static final class Result {
158
159 @Retention(RetentionPolicy.SOURCE)
160 @IntDef({CACHE_MISS, CACHE_HIT_EXACT, CACHE_HIT_SMALLER, CACHE_HIT_LARGER})
161 @interface Status {}
Garfield, Tanc8099c02016-05-02 12:01:30 -0700162 /**
163 * Indicates there is no thumbnail for the requested uri. The thumbnail will be null.
164 */
165 public static final int CACHE_MISS = 0;
166 /**
167 * Indicates the thumbnail matches the requested size and requested uri.
168 */
169 public static final int CACHE_HIT_EXACT = 1;
170 /**
171 * Indicates the thumbnail is in a smaller size than the requested one from the requested
172 * uri.
173 */
174 public static final int CACHE_HIT_SMALLER = 2;
175 /**
176 * Indicates the thumbnail is in a larger size than the requested one from the requested
177 * uri.
178 */
179 public static final int CACHE_HIT_LARGER = 3;
180
181 private static final Pools.SimplePool<Result> sPool = new Pools.SimplePool<>(1);
182
183 private @Status int mStatus;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700184 private @Nullable Bitmap mThumbnail;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700185 private @Nullable Point mSize;
Garfield, Tan16502a82016-05-19 15:36:36 -0700186 private long mLastModified;
187
188 private static Result obtainMiss() {
189 return obtain(CACHE_MISS, null, null, 0);
190 }
191
192 private static Result obtain(@Status int status, Point size, Entry entry) {
193 return obtain(status, entry.mThumbnail, size, entry.mLastModified);
194 }
Garfield, Tanc8099c02016-05-02 12:01:30 -0700195
196 private static Result obtain(@Status int status, @Nullable Bitmap thumbnail,
Garfield, Tan16502a82016-05-19 15:36:36 -0700197 @Nullable Point size, long lastModified) {
Garfield, Tanc8099c02016-05-02 12:01:30 -0700198 Result instance = sPool.acquire();
199 instance = (instance != null ? instance : new Result());
200
201 instance.mStatus = status;
202 instance.mThumbnail = thumbnail;
203 instance.mSize = size;
Garfield, Tan16502a82016-05-19 15:36:36 -0700204 instance.mLastModified = lastModified;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700205
206 return instance;
207 }
208
Garfield, Tan16502a82016-05-19 15:36:36 -0700209 private Result() {}
Garfield, Tanc8099c02016-05-02 12:01:30 -0700210
211 public void recycle() {
212 mStatus = -1;
213 mThumbnail = null;
214 mSize = null;
Garfield, Tan16502a82016-05-19 15:36:36 -0700215 mLastModified = -1;
Garfield, Tanc8099c02016-05-02 12:01:30 -0700216
217 boolean released = sPool.release(this);
218 // This assert is used to guarantee we won't generate too many instances that can't be
219 // held in the pool, which indicates our pool size is too small.
220 //
221 // Right now one instance is enough because we expect all instances are only used in
222 // main thread.
223 assert (released);
224 }
225
226 public @Status int getStatus() {
227 return mStatus;
228 }
229
230 public @Nullable Bitmap getThumbnail() {
231 return mThumbnail;
232 }
233
234 public @Nullable Point getSize() {
235 return mSize;
236 }
237
Garfield, Tan16502a82016-05-19 15:36:36 -0700238 public long getLastModified() {
239 return mLastModified;
240 }
241
Garfield, Tanc8099c02016-05-02 12:01:30 -0700242 public boolean isHit() {
243 return (mStatus != CACHE_MISS);
244 }
245
246 public boolean isExactHit() {
247 return (mStatus == CACHE_HIT_EXACT);
248 }
249 }
250
Garfield, Tan16502a82016-05-19 15:36:36 -0700251 private static final class Entry {
252 private final Bitmap mThumbnail;
253 private final long mLastModified;
254
255 private Entry(Bitmap thumbnail, long lastModified) {
256 mThumbnail = thumbnail;
257 mLastModified = lastModified;
258 }
259 }
260
261 private final class Cache extends LruCache<Pair<Uri, Point>, Entry> {
262
Garfield, Tanc8099c02016-05-02 12:01:30 -0700263 private Cache(int maxSizeBytes) {
264 super(maxSizeBytes);
265 }
266
267 @Override
Garfield, Tan16502a82016-05-19 15:36:36 -0700268 protected int sizeOf(Pair<Uri, Point> key, Entry value) {
269 return value.mThumbnail.getByteCount();
270 }
271
272 @Override
273 protected void entryRemoved(
274 boolean evicted, Pair<Uri, Point> key, Entry oldValue, Entry newValue) {
275 if (newValue == null) {
276 removeKey(key.first, key.second);
277 }
Garfield, Tanc8099c02016-05-02 12:01:30 -0700278 }
279 }
280
281 private static final class SizeComparator implements Comparator<Point> {
282 @Override
283 public int compare(Point size0, Point size1) {
284 // Assume all sizes are roughly square, so we only compare them in one dimension.
285 return size0.x - size1.x;
286 }
Jeff Sharkey8a8fb672013-05-07 12:41:33 -0700287 }
288}