Home | History | Annotate | Download | only in common
      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