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 android.annotation.IntDef; 20 import android.annotation.Nullable; 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 import android.util.Pair; 27 import android.util.Pools; 28 29 import com.android.documentsui.base.Shared; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 import java.util.Comparator; 34 import java.util.HashMap; 35 import 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 */ 41 public 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 */ ThumbnailCache(int maxCacheSizeInBytes)57 public ThumbnailCache(int maxCacheSizeInBytes) { 58 mSizeIndex = new HashMap<>(); 59 mCache = new Cache(maxCacheSizeInBytes); 60 } 61 62 /** 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 */ getThumbnail(Uri uri, Point size)69 public Result getThumbnail(Uri uri, Point size) { 70 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. 74 return Result.obtainMiss(); 75 } 76 77 // Look for thumbnail of the same size. 78 Pair<Uri, Point> cacheKey = sizeMap.get(size); 79 if (cacheKey != null) { 80 Entry entry = mCache.get(cacheKey); 81 if (entry != null) { 82 return Result.obtain(Result.CACHE_HIT_EXACT, size, entry); 83 } 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) { 92 Entry entry = mCache.get(cacheKey); 93 if (entry != null) { 94 return Result.obtain(Result.CACHE_HIT_LARGER, otherSize, entry); 95 } 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) { 105 Entry entry = mCache.get(cacheKey); 106 if (entry != null) { 107 return Result.obtain(Result.CACHE_HIT_SMALLER, otherSize, entry); 108 } 109 } 110 } 111 112 // Cache miss. 113 return Result.obtainMiss(); 114 } 115 116 /** 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 */ putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified)123 public void putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified) { 124 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 135 Entry entry = new Entry(thumbnail, lastModified); 136 mCache.put(cacheKey, entry); 137 synchronized (sizeMap) { 138 sizeMap.put(size, cacheKey); 139 } 140 } 141 142 /** 143 * Removes all thumbnail cache associated to the given uri. 144 * @param uri the uri which thumbnail cache to remove 145 */ removeUri(Uri uri)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 removeKey(Uri uri, Point size)162 private void removeKey(Uri uri, Point size) { 163 TreeMap<Point, Pair<Uri, Point>> sizeMap; 164 synchronized (mSizeIndex) { 165 sizeMap = mSizeIndex.get(uri); 166 } 167 168 assert(sizeMap != null); 169 synchronized (sizeMap) { 170 sizeMap.remove(size); 171 } 172 } 173 onTrimMemory(int level)174 public void onTrimMemory(int level) { 175 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { 176 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 {} 190 /** 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; 212 private @Nullable Bitmap mThumbnail; 213 private @Nullable Point mSize; 214 private long mLastModified; 215 obtainMiss()216 private static Result obtainMiss() { 217 return obtain(CACHE_MISS, null, null, 0); 218 } 219 obtain(@tatus int status, Point size, Entry entry)220 private static Result obtain(@Status int status, Point size, Entry entry) { 221 return obtain(status, entry.mThumbnail, size, entry.mLastModified); 222 } 223 obtain(@tatus int status, @Nullable Bitmap thumbnail, @Nullable Point size, long lastModified)224 private static Result obtain(@Status int status, @Nullable Bitmap thumbnail, 225 @Nullable Point size, long lastModified) { 226 Shared.checkMainLoop(); 227 228 Result instance = sPool.acquire(); 229 instance = (instance != null ? instance : new Result()); 230 231 instance.mStatus = status; 232 instance.mThumbnail = thumbnail; 233 instance.mSize = size; 234 instance.mLastModified = lastModified; 235 236 return instance; 237 } 238 Result()239 private Result() {} 240 recycle()241 public void recycle() { 242 Shared.checkMainLoop(); 243 244 mStatus = -1; 245 mThumbnail = null; 246 mSize = null; 247 mLastModified = -1; 248 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 getStatus()258 public @Status int getStatus() { 259 return mStatus; 260 } 261 getThumbnail()262 public @Nullable Bitmap getThumbnail() { 263 return mThumbnail; 264 } 265 getSize()266 public @Nullable Point getSize() { 267 return mSize; 268 } 269 getLastModified()270 public long getLastModified() { 271 return mLastModified; 272 } 273 isHit()274 public boolean isHit() { 275 return (mStatus != CACHE_MISS); 276 } 277 isExactHit()278 public boolean isExactHit() { 279 return (mStatus == CACHE_HIT_EXACT); 280 } 281 } 282 283 private static final class Entry { 284 private final Bitmap mThumbnail; 285 private final long mLastModified; 286 Entry(Bitmap thumbnail, long lastModified)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 Cache(int maxSizeBytes)295 private Cache(int maxSizeBytes) { 296 super(maxSizeBytes); 297 } 298 299 @Override sizeOf(Pair<Uri, Point> key, Entry value)300 protected int sizeOf(Pair<Uri, Point> key, Entry value) { 301 return value.mThumbnail.getByteCount(); 302 } 303 304 @Override entryRemoved( boolean evicted, Pair<Uri, Point> key, Entry oldValue, Entry newValue)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 } 310 } 311 } 312 313 private static final class SizeComparator implements Comparator<Point> { 314 @Override compare(Point size0, Point size1)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 } 319 } 320 } 321