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