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