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.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