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