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