Home | History | Annotate | Download | only in documentsui
      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      */
     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      */
     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      */
    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      */
    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 
    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 
    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 
    216         private static Result obtainMiss() {
    217             return obtain(CACHE_MISS, null, null, 0);
    218         }
    219 
    220         private static Result obtain(@Status int status, Point size, Entry entry) {
    221             return obtain(status, entry.mThumbnail, size, entry.mLastModified);
    222         }
    223 
    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 
    239         private Result() {}
    240 
    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 
    258         public @Status int getStatus() {
    259             return mStatus;
    260         }
    261 
    262         public @Nullable Bitmap getThumbnail() {
    263             return mThumbnail;
    264         }
    265 
    266         public @Nullable Point getSize() {
    267             return mSize;
    268         }
    269 
    270         public long getLastModified() {
    271             return mLastModified;
    272         }
    273 
    274         public boolean isHit() {
    275             return (mStatus != CACHE_MISS);
    276         }
    277 
    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 
    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 
    295         private Cache(int maxSizeBytes) {
    296             super(maxSizeBytes);
    297         }
    298 
    299         @Override
    300         protected int sizeOf(Pair<Uri, Point> key, Entry value) {
    301             return value.mThumbnail.getByteCount();
    302         }
    303 
    304         @Override
    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
    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