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