1/* 2 * Copyright (C) 2014 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 17package com.android.tv.settings.widget; 18 19import android.app.ActivityManager; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.graphics.Bitmap; 23import android.util.Log; 24import android.util.LruCache; 25import android.widget.ImageView; 26 27import com.android.tv.settings.R; 28import com.android.tv.settings.util.AccountImageChangeObserver; 29import com.android.tv.settings.util.UriUtils; 30 31import java.lang.ref.SoftReference; 32import java.util.concurrent.Executor; 33import java.util.concurrent.Executors; 34import java.util.Map; 35 36/** 37 * Downloader class which loads a resource URI into an image view. 38 * <p> 39 * This class adds a cache over BitmapWorkerTask. 40 */ 41public class BitmapDownloader { 42 43 private static final String TAG = "BitmapDownloader"; 44 45 private static final boolean DEBUG = false; 46 47 private static final int CORE_POOL_SIZE = 5; 48 49 private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR = 50 Executors.newFixedThreadPool(CORE_POOL_SIZE); 51 52 // 1/4 of max memory is used for bitmap mem cache 53 private static final int MEM_TO_CACHE = 4; 54 55 // hard limit for bitmap mem cache in MB 56 private static final int CACHE_HARD_LIMIT = 32; 57 58 /** 59 * bitmap cache item structure saved in LruCache 60 */ 61 private static class BitmapItem { 62 /** 63 * cached bitmap 64 */ 65 final Bitmap mBitmap; 66 /** 67 * indicate if the bitmap is scaled down from original source (never scale up) 68 */ 69 final boolean mScaled; 70 71 public BitmapItem(Bitmap bitmap, boolean scaled) { 72 mBitmap = bitmap; 73 mScaled = scaled; 74 } 75 } 76 77 private final LruCache<String, BitmapItem> mMemoryCache; 78 79 private static BitmapDownloader sBitmapDownloader; 80 81 private static final Object sBitmapDownloaderLock = new Object(); 82 83 // Bitmap cache also uses size of Bitmap as part of key. 84 // Bitmap cache is divided into following buckets by height: 85 // TODO: we currently care more about height, what about width in key? 86 // height <= 128, 128 < height <= 512, height > 512 87 // Different bitmap cache buckets save different bitmap cache items. 88 // Bitmaps within same bucket share the largest cache item. 89 private static final int[] SIZE_BUCKET = new int[]{128, 512, Integer.MAX_VALUE}; 90 91 public static abstract class BitmapCallback { 92 SoftReference<BitmapWorkerTask> mTask; 93 94 public abstract void onBitmapRetrieved(Bitmap bitmap); 95 } 96 97 /** 98 * get the singleton BitmapDownloader for the application 99 */ 100 public static BitmapDownloader getInstance(Context context) { 101 if (sBitmapDownloader == null) { 102 synchronized(sBitmapDownloaderLock) { 103 if (sBitmapDownloader == null) { 104 sBitmapDownloader = new BitmapDownloader(context); 105 } 106 } 107 } 108 return sBitmapDownloader; 109 } 110 111 public BitmapDownloader(Context context) { 112 int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)) 113 .getMemoryClass(); 114 memClass = memClass / MEM_TO_CACHE; 115 if (memClass > CACHE_HARD_LIMIT) { 116 memClass = CACHE_HARD_LIMIT; 117 } 118 int cacheSize = 1024 * 1024 * memClass; 119 mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) { 120 @Override 121 protected int sizeOf(String key, BitmapItem bitmap) { 122 return bitmap.mBitmap.getByteCount(); 123 } 124 }; 125 } 126 127 /** 128 * load bitmap in current thread, will *block* current thread. 129 * FIXME: Should avoid using this function at all cost. 130 * @deprecated 131 */ 132 @Deprecated 133 public final Bitmap loadBitmapBlocking(BitmapWorkerOptions options) { 134 final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri()); 135 Bitmap bitmap = null; 136 if (hasAccountImageUri) { 137 AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options); 138 } else { 139 bitmap = getBitmapFromMemCache(options); 140 } 141 142 if (bitmap == null) { 143 BitmapWorkerTask task = new BitmapWorkerTask(null) { 144 @Override 145 protected Bitmap doInBackground(BitmapWorkerOptions... params) { 146 final Bitmap bitmap = super.doInBackground(params); 147 if (bitmap != null && !hasAccountImageUri) { 148 addBitmapToMemoryCache(params[0], bitmap, isScaled()); 149 } 150 return bitmap; 151 } 152 }; 153 154 return task.doInBackground(options); 155 } 156 return bitmap; 157 } 158 159 /** 160 * Loads the bitmap into the image view. 161 */ 162 public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) { 163 cancelDownload(imageView); 164 final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri()); 165 Bitmap bitmap = null; 166 if (hasAccountImageUri) { 167 AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options); 168 } else { 169 bitmap = getBitmapFromMemCache(options); 170 } 171 172 if (bitmap != null) { 173 imageView.setImageBitmap(bitmap); 174 } else { 175 BitmapWorkerTask task = new BitmapWorkerTask(imageView) { 176 @Override 177 protected Bitmap doInBackground(BitmapWorkerOptions... params) { 178 Bitmap bitmap = super.doInBackground(params); 179 if (bitmap != null && !hasAccountImageUri) { 180 addBitmapToMemoryCache(params[0], bitmap, isScaled()); 181 } 182 return bitmap; 183 } 184 }; 185 imageView.setTag(R.id.imageDownloadTask, new SoftReference<>(task)); 186 task.execute(options); 187 } 188 } 189 190 /** 191 * Loads the bitmap. 192 * <p> 193 * This will be sent back to the callback object. 194 */ 195 public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) { 196 cancelDownload(callback); 197 final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri()); 198 final Bitmap bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options); 199 if (hasAccountImageUri) { 200 AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options); 201 } 202 203 BitmapWorkerTask task = new BitmapWorkerTask(null) { 204 @Override 205 protected Bitmap doInBackground(BitmapWorkerOptions... params) { 206 if (bitmap != null) { 207 return bitmap; 208 } 209 final Bitmap bitmap = super.doInBackground(params); 210 if (bitmap != null && !hasAccountImageUri) { 211 addBitmapToMemoryCache(params[0], bitmap, isScaled()); 212 } 213 return bitmap; 214 } 215 216 @Override 217 protected void onPostExecute(Bitmap bitmap) { 218 callback.onBitmapRetrieved(bitmap); 219 } 220 }; 221 callback.mTask = new SoftReference<>(task); 222 task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options); 223 } 224 225 /** 226 * Cancel download<p> 227 * @param key {@link BitmapCallback} or {@link ImageView} 228 */ 229 public boolean cancelDownload(Object key) { 230 BitmapWorkerTask task = null; 231 if (key instanceof ImageView) { 232 ImageView imageView = (ImageView)key; 233 SoftReference<BitmapWorkerTask> softReference = 234 (SoftReference<BitmapWorkerTask>) imageView.getTag(R.id.imageDownloadTask); 235 if (softReference != null) { 236 task = softReference.get(); 237 softReference.clear(); 238 } 239 } else if (key instanceof BitmapCallback) { 240 BitmapCallback callback = (BitmapCallback)key; 241 if (callback.mTask != null) { 242 task = callback.mTask.get(); 243 callback.mTask = null; 244 } 245 } 246 if (task != null) { 247 return task.cancel(true); 248 } 249 return false; 250 } 251 252 private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig, int width) { 253 for (int i = 0; i < SIZE_BUCKET.length; i++) { 254 if (width <= SIZE_BUCKET[i]) { 255 return new StringBuilder(baseKey.length() + 16).append(baseKey) 256 .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal()) 257 .append(":").append(SIZE_BUCKET[i]).toString(); 258 } 259 } 260 // should never happen because last bucket is Integer.MAX_VALUE 261 throw new RuntimeException(); 262 } 263 264 private void addBitmapToMemoryCache(BitmapWorkerOptions key, Bitmap bitmap, boolean isScaled) { 265 if (!key.isMemCacheEnabled()) { 266 return; 267 } 268 String bucketKey = getBucketKey( 269 key.getCacheKey(), key.getBitmapConfig(), bitmap.getHeight()); 270 BitmapItem bitmapItem = mMemoryCache.get(bucketKey); 271 if (bitmapItem != null) { 272 Bitmap currentBitmap = bitmapItem.mBitmap; 273 // If somebody else happened to get a larger one in the bucket, discard our bitmap. 274 // TODO: need a better way to prevent current downloading for the same Bitmap 275 if (currentBitmap.getWidth() >= bitmap.getWidth() && currentBitmap.getHeight() 276 >= bitmap.getHeight()) { 277 return; 278 } 279 } 280 if (DEBUG) { 281 Log.d(TAG, "add cache "+bucketKey+" isScaled = "+isScaled); 282 } 283 bitmapItem = new BitmapItem(bitmap, isScaled); 284 mMemoryCache.put(bucketKey, bitmapItem); 285 } 286 287 private Bitmap getBitmapFromMemCache(BitmapWorkerOptions key) { 288 if (key.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) { 289 // 1. find the bitmap in the size bucket 290 String bucketKey = 291 getBucketKey(key.getCacheKey(), key.getBitmapConfig(), key.getHeight()); 292 BitmapItem bitmapItem = mMemoryCache.get(bucketKey); 293 if (bitmapItem != null) { 294 Bitmap bitmap = bitmapItem.mBitmap; 295 // now we have the bitmap in the bucket, use it when the bitmap is not scaled or 296 // if the size is larger than or equals to the output size 297 if (!bitmapItem.mScaled) { 298 return bitmap; 299 } 300 if (bitmap.getHeight() >= key.getHeight()) { 301 return bitmap; 302 } 303 } 304 // 2. find un-scaled bitmap in smaller buckets. If the un-scaled bitmap exists 305 // in higher buckets, we still need to scale it down. Right now we just 306 // return null and let the BitmapWorkerTask to do the same job again. 307 // TODO: use the existing unscaled bitmap and we don't need to load it from resource 308 // or network again. 309 for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) { 310 if (SIZE_BUCKET[i] >= key.getHeight()) { 311 continue; 312 } 313 bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]); 314 bitmapItem = mMemoryCache.get(bucketKey); 315 if (bitmapItem != null && !bitmapItem.mScaled) { 316 return bitmapItem.mBitmap; 317 } 318 } 319 return null; 320 } 321 // 3. find un-scaled bitmap if size is not specified 322 for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) { 323 String bucketKey = 324 getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]); 325 BitmapItem bitmapItem = mMemoryCache.get(bucketKey); 326 if (bitmapItem != null && !bitmapItem.mScaled) { 327 return bitmapItem.mBitmap; 328 } 329 } 330 return null; 331 } 332 333 public Bitmap getLargestBitmapFromMemCache(BitmapWorkerOptions key) { 334 // find largest bitmap matching the key 335 for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) { 336 String bucketKey = 337 getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]); 338 BitmapItem bitmapItem = mMemoryCache.get(bucketKey); 339 if (bitmapItem != null) { 340 return bitmapItem.mBitmap; 341 } 342 } 343 return null; 344 } 345 346 public void invalidateCachedResources() { 347 Map<String, BitmapItem> snapshot = mMemoryCache.snapshot(); 348 for (String uri: snapshot.keySet()) { 349 Log.d(TAG, "remove cached image: " + uri); 350 if (uri.startsWith(ContentResolver.SCHEME_ANDROID_RESOURCE)) { 351 mMemoryCache.remove(uri); 352 } 353 } 354 } 355} 356