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