Home | History | Annotate | Download | only in widget
      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.util.UriUtils;
     31 import com.android.tv.settings.util.AccountImageChangeObserver;
     32 import com.android.tv.settings.R;
     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         int mOriginalWidth;
     82         int mOriginalHeight;
     83         ArrayList<BitmapDrawable> mBitmaps = new ArrayList<BitmapDrawable>(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 Context mContext;
    149     private LruCache<String, BitmapItem> mMemoryCache;
    150     private 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