1 /*
2  * Copyright (C) 2015 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.tv.util;
18 
19 import android.support.annotation.VisibleForTesting;
20 import android.util.Log;
21 import android.util.LruCache;
22 
23 import com.android.tv.common.MemoryManageable;
24 import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
25 
26 /**
27  * A convenience class for caching bitmap.
28  */
29 public class ImageCache implements MemoryManageable {
30     private static final float MAX_CACHE_SIZE_PERCENT = 0.8f;
31     private static final float MIN_CACHE_SIZE_PERCENT = 0.05f;
32     private static final float DEFAULT_CACHE_SIZE_PERCENT = 0.1f;
33     private static final boolean DEBUG = false;
34     private static final String TAG = "ImageCache";
35     private static final int MIN_CACHE_SIZE_KBYTES = 1024;
36 
37     private final LruCache<String, ScaledBitmapInfo> mMemoryCache;
38 
39     /**
40      * Creates a new ImageCache object with a given cache size percent.
41      *
42      * @param memCacheSizePercent The cache size as a percent of available app memory.
43      */
ImageCache(float memCacheSizePercent)44     private ImageCache(float memCacheSizePercent) {
45         int memCacheSize = calculateMemCacheSize(memCacheSizePercent);
46 
47         // Set up memory cache
48         if (DEBUG) {
49             Log.d(TAG, "Memory cache created (size = " + memCacheSize + " Kbytes)");
50         }
51         mMemoryCache = new LruCache<String, ScaledBitmapInfo>(memCacheSize) {
52             /**
53              * Measure item size in kilobytes rather than units which is more practical for a bitmap
54              * cache
55              */
56             @Override
57             protected int sizeOf(String key, ScaledBitmapInfo bitmapInfo) {
58                 return (bitmapInfo.bitmap.getByteCount() + 1023) / 1024;
59             }
60         };
61     }
62 
63     private static ImageCache sImageCache;
64 
65     /**
66      * Returns an existing ImageCache, if it doesn't exist, a new one is created using the supplied
67      * param.
68      *
69      * @param memCacheSizePercent The cache size as a percent of available app memory. Should be in
70      *                            range of MIN_CACHE_SIZE_PERCENT(0.05) ~ MAX_CACHE_SIZE_PERCENT(0.8).
71      * @return An existing retained ImageCache object or a new one if one did not exist
72      */
getInstance(float memCacheSizePercent)73     public static synchronized ImageCache getInstance(float memCacheSizePercent) {
74         if (sImageCache == null) {
75             sImageCache = newInstance(memCacheSizePercent);
76         }
77         return sImageCache;
78     }
79 
80     @VisibleForTesting
newInstance(float memCacheSizePercent)81     static ImageCache newInstance(float memCacheSizePercent) {
82         return new ImageCache(memCacheSizePercent);
83     }
84 
85 
86     /**
87      * Returns an existing ImageCache, if it doesn't exist, a new one is created using
88      * DEFAULT_CACHE_SIZE_PERCENT (0.1).
89      *
90      * @return An existing retained ImageCache object or a new one if one did not exist
91      */
getInstance()92     public static ImageCache getInstance() {
93         return getInstance(DEFAULT_CACHE_SIZE_PERCENT);
94     }
95 
96     /**
97      * Adds a bitmap to memory cache.
98      *
99      * <p>If there is an existing bitmap only replace it if
100      * {@link ScaledBitmapInfo#needToReload(ScaledBitmapInfo)} is true.
101      *
102      * @param bitmapInfo The {@link ScaledBitmapInfo} object to store
103      */
putIfNeeded(ScaledBitmapInfo bitmapInfo)104     public void putIfNeeded(ScaledBitmapInfo bitmapInfo) {
105         if (bitmapInfo == null || bitmapInfo.id == null) {
106             throw new IllegalArgumentException("Neither bitmap nor bitmap.id should be null.");
107         }
108         String key = bitmapInfo.id;
109         // Add to memory cache
110         synchronized (mMemoryCache) {
111             ScaledBitmapInfo old = mMemoryCache.put(key, bitmapInfo);
112             if (old != null && !old.needToReload(bitmapInfo)) {
113                 mMemoryCache.put(key, old);
114                 if (DEBUG) {
115                     Log.d(TAG,
116                             "Kept original " + old + " in memory cache because it was larger than "
117                                     + bitmapInfo + ".");
118                 }
119             } else {
120                 if (DEBUG) {
121                     Log.d(TAG, "Add " + bitmapInfo + " to memory cache. Current size is " +
122                             mMemoryCache.size() + " / " + mMemoryCache.maxSize() + " Kbytes");
123                 }
124             }
125         }
126     }
127 
128     /**
129      * Get from memory cache.
130      *
131      * @param key Unique identifier for which item to get
132      * @return The bitmap if found in cache, null otherwise
133      */
get(String key)134     public ScaledBitmapInfo get(String key) {
135         ScaledBitmapInfo memBitmapInfo = mMemoryCache.get(key);
136         if (DEBUG) {
137             int hit = mMemoryCache.hitCount();
138             int miss = mMemoryCache.missCount();
139             String result = memBitmapInfo == null ? "miss" : "hit";
140             double ratio = ((double) hit) / (hit + miss) * 100;
141             Log.d(TAG, "Memory cache " + result + " for  " + key);
142             Log.d(TAG, "Memory cache " + hit + "h:" + miss + "m " + ratio + "%");
143         }
144         return memBitmapInfo;
145     }
146 
147     /**
148      * Remove from memory cache.
149      *
150      * @param key Unique identifier for which item to remove
151      * @return The previous bitmap mapped by key
152      */
remove(String key)153     public ScaledBitmapInfo remove(String key) {
154         return mMemoryCache.remove(key);
155     }
156 
157     /**
158      * Calculates the memory cache size based on a percentage of the max available VM memory. Eg.
159      * setting percent to 0.2 would set the memory cache to one fifth of the available memory.
160      * Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8. memCacheSize is stored
161      * in kilobytes instead of bytes as this will eventually be passed to construct a LruCache
162      * which takes an int in its constructor. This value should be chosen carefully based on a
163      * number of factors Refer to the corresponding Android Training class for more discussion:
164      * http://developer.android.com/training/displaying-bitmaps/
165      *
166      * @param percent Percent of available app memory to use to size memory cache.
167      */
calculateMemCacheSize(float percent)168     public static int calculateMemCacheSize(float percent) {
169         if (percent < MIN_CACHE_SIZE_PERCENT || percent > MAX_CACHE_SIZE_PERCENT) {
170             throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
171                     + "between 0.05 and 0.8 (inclusive)");
172         }
173         return Math.max(MIN_CACHE_SIZE_KBYTES,
174                 Math.round(percent * Runtime.getRuntime().maxMemory() / 1024));
175     }
176 
177     @Override
performTrimMemory(int level)178     public void performTrimMemory(int level) {
179         mMemoryCache.evictAll();
180     }
181 }
182