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.content.Intent.ShortcutIconResource; 22 import android.content.pm.PackageManager.NameNotFoundException; 23 import android.graphics.Bitmap; 24 import android.graphics.drawable.BitmapDrawable; 25 import android.graphics.drawable.Drawable; 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.ArrayList; 36 import java.util.concurrent.Executor; 37 import java.util.concurrent.Executors; 38 39 /** 40 * Downloader class which loads a resource URI into an image view or triggers a callback 41 * <p> 42 * This class adds a LRU cache over DrawableLoader. 43 * <p> 44 * Calling getBitmap() or loadBitmap() will return a RefcountBitmapDrawable with initial refcount = 45 * 2 by the cache table and by caller. You must call releaseRef() when you are done with the resource. 46 * The most common way is using RefcountImageView, and releaseRef() for you. Once both RefcountImageView 47 * and LRUCache removes the refcount, the underlying bitmap will be used for decoding new bitmap. 48 * <p> 49 * If the URI does not point to a bitmap (e.g. point to a drawable xml, we won't cache it and we 50 * directly return a regular Drawable). 51 */ 52 public class DrawableDownloader { 53 54 private static final String TAG = "DrawableDownloader"; 55 56 private static final boolean DEBUG = false; 57 58 private static final int CORE_POOL_SIZE = 5; 59 60 // thread pool for loading non android-resources such as http, content 61 private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR = 62 Executors.newFixedThreadPool(CORE_POOL_SIZE); 63 64 private static final int CORE_RESOURCE_POOL_SIZE = 1; 65 66 // thread pool for loading android resources, we use separate thread pool so 67 // that network loading will not block local android icons 68 private static final Executor BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR = 69 Executors.newFixedThreadPool(CORE_RESOURCE_POOL_SIZE); 70 71 // 1/4 of max memory is used for bitmap mem cache 72 private static final int MEM_TO_CACHE = 4; 73 74 // hard limit for bitmap mem cache in MB 75 private static final int CACHE_HARD_LIMIT = 32; 76 77 /** 78 * bitmap cache item structure saved in LruCache 79 */ 80 private static class BitmapItem { 81 final int mOriginalWidth; 82 final int mOriginalHeight; 83 final ArrayList<BitmapDrawable> mBitmaps = new ArrayList<>(3); 84 int mByteCount; 85 public BitmapItem(int originalWidth, int originalHeight) { 86 mOriginalWidth = originalWidth; 87 mOriginalHeight = originalHeight; 88 } 89 90 // get bitmap from the list 91 BitmapDrawable findDrawable(BitmapWorkerOptions options) { 92 for (int i = 0, c = mBitmaps.size(); i < c; i++) { 93 BitmapDrawable d = mBitmaps.get(i); 94 // use drawable with original size 95 if (d.getIntrinsicWidth() == mOriginalWidth 96 && d.getIntrinsicHeight() == mOriginalHeight) { 97 return d; 98 } 99 // if specified width/height in options and is smaller than 100 // cached one, we can use this cached drawable 101 if (options.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) { 102 if (options.getHeight() <= d.getIntrinsicHeight()) { 103 return d; 104 } 105 } else if (options.getWidth() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) { 106 if (options.getWidth() <= d.getIntrinsicWidth()) { 107 return d; 108 } 109 } 110 } 111 return null; 112 } 113 114 BitmapDrawable findLargestDrawable(BitmapWorkerOptions options) { 115 return mBitmaps.size() == 0 ? null : mBitmaps.get(0); 116 } 117 118 void addDrawable(BitmapDrawable d) { 119 int i = 0, c = mBitmaps.size(); 120 for (; i < c; i++) { 121 BitmapDrawable drawable = mBitmaps.get(i); 122 if (drawable.getIntrinsicHeight() < d.getIntrinsicHeight()) { 123 break; 124 } 125 } 126 mBitmaps.add(i, d); 127 mByteCount += RecycleBitmapPool.getSize(d.getBitmap()); 128 } 129 130 void clear() { 131 for (int i = 0, c = mBitmaps.size(); i < c; i++) { 132 BitmapDrawable d = mBitmaps.get(i); 133 if (d instanceof RefcountBitmapDrawable) { 134 ((RefcountBitmapDrawable) d).getRefcountObject().releaseRef(); 135 } 136 } 137 mBitmaps.clear(); 138 mByteCount = 0; 139 } 140 } 141 142 public static abstract class BitmapCallback { 143 SoftReference<DrawableLoader> mTask; 144 145 public abstract void onBitmapRetrieved(Drawable bitmap); 146 } 147 148 private final Context mContext; 149 private final LruCache<String, BitmapItem> mMemoryCache; 150 private final RecycleBitmapPool mRecycledBitmaps; 151 152 private static DrawableDownloader sBitmapDownloader; 153 154 private static final Object sBitmapDownloaderLock = new Object(); 155 156 /** 157 * get the singleton BitmapDownloader for the application 158 */ 159 public final static DrawableDownloader getInstance(Context context) { 160 if (sBitmapDownloader == null) { 161 synchronized(sBitmapDownloaderLock) { 162 if (sBitmapDownloader == null) { 163 sBitmapDownloader = new DrawableDownloader(context); 164 } 165 } 166 } 167 return sBitmapDownloader; 168 } 169 170 private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig) { 171 return new StringBuilder(baseKey.length() + 16).append(baseKey) 172 .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal()) 173 .toString(); 174 } 175 176 public static Drawable getDrawable(Context context, ShortcutIconResource iconResource) 177 throws NameNotFoundException { 178 return DrawableLoader.getDrawable(context, iconResource); 179 } 180 181 private DrawableDownloader(Context context) { 182 mContext = context; 183 int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)) 184 .getMemoryClass(); 185 memClass = memClass / MEM_TO_CACHE; 186 if (memClass > CACHE_HARD_LIMIT) { 187 memClass = CACHE_HARD_LIMIT; 188 } 189 int cacheSize = 1024 * 1024 * memClass; 190 mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) { 191 @Override 192 protected int sizeOf(String key, BitmapItem bitmap) { 193 return bitmap.mByteCount; 194 } 195 @Override 196 protected void entryRemoved( 197 boolean evicted, String key, BitmapItem oldValue, BitmapItem newValue) { 198 if (evicted) { 199 oldValue.clear(); 200 } 201 } 202 }; 203 mRecycledBitmaps = new RecycleBitmapPool(); 204 } 205 206 /** 207 * trim memory cache to 0~1 * maxSize 208 */ 209 public void trimTo(float amount) { 210 if (amount == 0f) { 211 mMemoryCache.evictAll(); 212 } else { 213 mMemoryCache.trimToSize((int) (amount * mMemoryCache.maxSize())); 214 } 215 } 216 217 /** 218 * load bitmap in current thread, will *block* current thread. 219 * FIXME: Should avoid using this function at all cost. 220 * @deprecated 221 */ 222 @Deprecated 223 public final Drawable loadBitmapBlocking(BitmapWorkerOptions options) { 224 final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri()); 225 Drawable bitmap = null; 226 if (hasAccountImageUri) { 227 AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options); 228 } else { 229 bitmap = getBitmapFromMemCache(options); 230 } 231 232 if (bitmap == null) { 233 DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) { 234 @Override 235 protected Drawable doInBackground(BitmapWorkerOptions... params) { 236 final Drawable bitmap = super.doInBackground(params); 237 if (bitmap != null && !hasAccountImageUri) { 238 addBitmapToMemoryCache(params[0], bitmap, this); 239 } 240 return bitmap; 241 } 242 }; 243 return task.doInBackground(options); 244 } 245 return bitmap; 246 } 247 248 /** 249 * Loads the bitmap into the image view. 250 */ 251 public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) { 252 cancelDownload(imageView); 253 final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri()); 254 Drawable bitmap = null; 255 if (hasAccountImageUri) { 256 AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options); 257 } else { 258 bitmap = getBitmapFromMemCache(options); 259 } 260 261 if (bitmap != null) { 262 imageView.setImageDrawable(bitmap); 263 } else { 264 DrawableLoader task = new DrawableLoader(imageView, mRecycledBitmaps) { 265 @Override 266 protected Drawable doInBackground(BitmapWorkerOptions... params) { 267 Drawable bitmap = super.doInBackground(params); 268 if (bitmap != null && !hasAccountImageUri) { 269 addBitmapToMemoryCache(params[0], bitmap, this); 270 } 271 return bitmap; 272 } 273 }; 274 imageView.setTag(R.id.imageDownloadTask, new SoftReference<DrawableLoader>(task)); 275 scheduleTask(task, options); 276 } 277 } 278 279 /** 280 * Loads the bitmap. 281 * <p> 282 * This will be sent back to the callback object. 283 */ 284 public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) { 285 cancelDownload(callback); 286 final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri()); 287 final Drawable bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options); 288 if (hasAccountImageUri) { 289 AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options); 290 } 291 292 if (bitmap != null) { 293 callback.onBitmapRetrieved(bitmap); 294 return; 295 } 296 DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) { 297 @Override 298 protected Drawable doInBackground(BitmapWorkerOptions... params) { 299 final Drawable bitmap = super.doInBackground(params); 300 if (bitmap != null && !hasAccountImageUri) { 301 addBitmapToMemoryCache(params[0], bitmap, this); 302 } 303 return bitmap; 304 } 305 306 @Override 307 protected void onPostExecute(Drawable bitmap) { 308 callback.onBitmapRetrieved(bitmap); 309 } 310 }; 311 callback.mTask = new SoftReference<DrawableLoader>(task); 312 scheduleTask(task, options); 313 } 314 315 private static void scheduleTask(DrawableLoader task, BitmapWorkerOptions options) { 316 if (options.isFromResource()) { 317 task.executeOnExecutor(BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR, options); 318 } else { 319 task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options); 320 } 321 } 322 323 /** 324 * Cancel download<p> 325 * @param key {@link BitmapCallback} or {@link ImageView} 326 */ 327 public boolean cancelDownload(Object key) { 328 DrawableLoader task = null; 329 if (key instanceof ImageView) { 330 ImageView imageView = (ImageView)key; 331 SoftReference<DrawableLoader> softReference = 332 (SoftReference<DrawableLoader>) imageView.getTag(R.id.imageDownloadTask); 333 if (softReference != null) { 334 task = softReference.get(); 335 softReference.clear(); 336 } 337 } else if (key instanceof BitmapCallback) { 338 BitmapCallback callback = (BitmapCallback)key; 339 if (callback.mTask != null) { 340 task = callback.mTask.get(); 341 callback.mTask = null; 342 } 343 } 344 if (task != null) { 345 return task.cancel(true); 346 } 347 return false; 348 } 349 350 private void addBitmapToMemoryCache(BitmapWorkerOptions key, Drawable bitmap, 351 DrawableLoader loader) { 352 if (!key.isMemCacheEnabled()) { 353 return; 354 } 355 if (!(bitmap instanceof BitmapDrawable)) { 356 return; 357 } 358 String bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig()); 359 BitmapItem bitmapItem = mMemoryCache.get(bucketKey); 360 if (DEBUG) { 361 Log.d(TAG, "add cache "+bucketKey); 362 } 363 if (bitmapItem != null) { 364 // remove and re-add to update size 365 mMemoryCache.remove(bucketKey); 366 } else { 367 bitmapItem = new BitmapItem(loader.getOriginalWidth(), loader.getOriginalHeight()); 368 } 369 if (bitmap instanceof RefcountBitmapDrawable) { 370 RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) bitmap; 371 refcountDrawable.getRefcountObject().addRef(); 372 } 373 bitmapItem.addDrawable((BitmapDrawable) bitmap); 374 mMemoryCache.put(bucketKey, bitmapItem); 375 } 376 377 private Drawable getBitmapFromMemCache(BitmapWorkerOptions key) { 378 String bucketKey = 379 getBucketKey(key.getCacheKey(), key.getBitmapConfig()); 380 BitmapItem item = mMemoryCache.get(bucketKey); 381 if (item != null) { 382 return createRefCopy(item.findDrawable(key)); 383 } 384 return null; 385 } 386 387 public BitmapDrawable getLargestBitmapFromMemCache(BitmapWorkerOptions key) { 388 String bucketKey = 389 getBucketKey(key.getCacheKey(), key.getBitmapConfig()); 390 BitmapItem item = mMemoryCache.get(bucketKey); 391 if (item != null) { 392 return (BitmapDrawable) createRefCopy(item.findLargestDrawable(key)); 393 } 394 return null; 395 } 396 397 private Drawable createRefCopy(Drawable d) { 398 if (d != null) { 399 if (d instanceof RefcountBitmapDrawable) { 400 RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) d; 401 refcountDrawable.getRefcountObject().addRef(); 402 d = new RefcountBitmapDrawable(mContext.getResources(), 403 refcountDrawable); 404 } 405 return d; 406 } 407 return null; 408 } 409 410 } 411