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