Use thumbnail of other sizes if it's missing in current size.

Bug: 26881628

Change-Id: Id7aa6f5c8c1a415f7dd97143a088ba89fae43eea
diff --git a/src/com/android/documentsui/ThumbnailCache.java b/src/com/android/documentsui/ThumbnailCache.java
index ad7cbf6..25cf806 100644
--- a/src/com/android/documentsui/ThumbnailCache.java
+++ b/src/com/android/documentsui/ThumbnailCache.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * 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.
@@ -16,17 +16,243 @@
 
 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;
 
-public class ThumbnailCache extends LruCache<Uri, Bitmap> {
-    public ThumbnailCache(int maxSizeBytes) {
-        super(maxSizeBytes);
+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);
     }
 
-    @Override
-    protected int sizeOf(Uri key, Bitmap value) {
-        return value.getByteCount();
+    /**
+     * 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) {
+        Result result = Result.obtain(Result.CACHE_MISS, null, null);
+
+        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;
+        }
+
+        // Look for thumbnail of the same size.
+        Pair<Uri, Point> cacheKey = sizeMap.get(size);
+        if (cacheKey != null) {
+            Bitmap thumbnail = mCache.get(cacheKey);
+            if (thumbnail != null) {
+                result.mStatus = Result.CACHE_HIT_EXACT;
+                result.mThumbnail = thumbnail;
+                result.mSize = size;
+                return result;
+            }
+        }
+
+        // Look for thumbnail of bigger sizes.
+        Point otherSize = sizeMap.higherKey(size);
+        if (otherSize != null) {
+            cacheKey = sizeMap.get(otherSize);
+
+            if (cacheKey != null) {
+                Bitmap thumbnail = mCache.get(cacheKey);
+                if (thumbnail != null) {
+                    result.mStatus = Result.CACHE_HIT_LARGER;
+                    result.mThumbnail = thumbnail;
+                    result.mSize = otherSize;
+                    return result;
+                }
+            }
+        }
+
+        // Look for thumbnail of smaller sizes.
+        otherSize = sizeMap.lowerKey(size);
+        if (otherSize != null) {
+            cacheKey = sizeMap.get(otherSize);
+
+            if (cacheKey != null) {
+                Bitmap thumbnail = mCache.get(cacheKey);
+                if (thumbnail != null) {
+                    result.mStatus = Result.CACHE_HIT_SMALLER;
+                    result.mThumbnail = thumbnail;
+                    result.mSize = otherSize;
+                    return result;
+                }
+            }
+        }
+
+        // Cache miss.
+        return result;
+    }
+
+    public void putThumbnail(Uri uri, Point size, Bitmap thumbnail) {
+        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);
+            }
+        }
+
+        mCache.put(cacheKey, thumbnail);
+        synchronized (sizeMap) {
+            sizeMap.put(size, cacheKey);
+        }
+    }
+
+    public void onTrimMemory(int level) {
+        if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+            synchronized (mSizeIndex) {
+                mSizeIndex.clear();
+            }
+            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 static Result obtain(@Status int status, @Nullable Bitmap thumbnail,
+                @Nullable Point size) {
+            Result instance = sPool.acquire();
+            instance = (instance != null ? instance : new Result());
+
+            instance.mStatus = status;
+            instance.mThumbnail = thumbnail;
+            instance.mSize = size;
+
+            return instance;
+        }
+
+        private Result() {
+        }
+
+        public void recycle() {
+            mStatus = -1;
+            mThumbnail = null;
+            mSize = null;
+
+            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 boolean isHit() {
+            return (mStatus != CACHE_MISS);
+        }
+
+        public boolean isExactHit() {
+            return (mStatus == CACHE_HIT_EXACT);
+        }
+    }
+
+    private static final class Cache extends LruCache<Pair<Uri, Point>, Bitmap> {
+        private Cache(int maxSizeBytes) {
+            super(maxSizeBytes);
+        }
+
+        @Override
+        protected int sizeOf(Pair<Uri, Point> key, Bitmap value) {
+            return value.getByteCount();
+        }
+    }
+
+    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;
+        }
     }
 }