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.ContentResolver;
     21 import android.content.Context;
     22 import android.graphics.Bitmap;
     23 import android.util.Log;
     24 import android.util.LruCache;
     25 import android.widget.ImageView;
     26 
     27 import com.android.tv.settings.R;
     28 import com.android.tv.settings.util.AccountImageChangeObserver;
     29 import com.android.tv.settings.util.UriUtils;
     30 
     31 import java.lang.ref.SoftReference;
     32 import java.util.concurrent.Executor;
     33 import java.util.concurrent.Executors;
     34 import java.util.Map;
     35 
     36 /**
     37  * Downloader class which loads a resource URI into an image view.
     38  * <p>
     39  * This class adds a cache over BitmapWorkerTask.
     40  */
     41 public class BitmapDownloader {
     42 
     43     private static final String TAG = "BitmapDownloader";
     44 
     45     private static final boolean DEBUG = false;
     46 
     47     private static final int CORE_POOL_SIZE = 5;
     48 
     49     private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR =
     50             Executors.newFixedThreadPool(CORE_POOL_SIZE);
     51 
     52     // 1/4 of max memory is used for bitmap mem cache
     53     private static final int MEM_TO_CACHE = 4;
     54 
     55     // hard limit for bitmap mem cache in MB
     56     private static final int CACHE_HARD_LIMIT = 32;
     57 
     58     /**
     59      * bitmap cache item structure saved in LruCache
     60      */
     61     private static class BitmapItem {
     62         /**
     63          * cached bitmap
     64          */
     65         final Bitmap mBitmap;
     66         /**
     67          * indicate if the bitmap is scaled down from original source (never scale up)
     68          */
     69         final boolean mScaled;
     70 
     71         public BitmapItem(Bitmap bitmap, boolean scaled) {
     72             mBitmap = bitmap;
     73             mScaled = scaled;
     74         }
     75     }
     76 
     77     private final LruCache<String, BitmapItem> mMemoryCache;
     78 
     79     private static BitmapDownloader sBitmapDownloader;
     80 
     81     private static final Object sBitmapDownloaderLock = new Object();
     82 
     83     // Bitmap cache also uses size of Bitmap as part of key.
     84     // Bitmap cache is divided into following buckets by height:
     85     // TODO: we currently care more about height, what about width in key?
     86     // height <= 128, 128 < height <= 512, height > 512
     87     // Different bitmap cache buckets save different bitmap cache items.
     88     // Bitmaps within same bucket share the largest cache item.
     89     private static final int[] SIZE_BUCKET = new int[]{128, 512, Integer.MAX_VALUE};
     90 
     91     public static abstract class BitmapCallback {
     92         SoftReference<BitmapWorkerTask> mTask;
     93 
     94         public abstract void onBitmapRetrieved(Bitmap bitmap);
     95     }
     96 
     97     /**
     98      * get the singleton BitmapDownloader for the application
     99      */
    100     public static BitmapDownloader getInstance(Context context) {
    101         if (sBitmapDownloader == null) {
    102             synchronized(sBitmapDownloaderLock) {
    103                 if (sBitmapDownloader == null) {
    104                     sBitmapDownloader = new BitmapDownloader(context);
    105                 }
    106             }
    107         }
    108         return sBitmapDownloader;
    109     }
    110 
    111     public BitmapDownloader(Context context) {
    112         int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
    113                 .getMemoryClass();
    114         memClass = memClass / MEM_TO_CACHE;
    115         if (memClass > CACHE_HARD_LIMIT) {
    116             memClass = CACHE_HARD_LIMIT;
    117         }
    118         int cacheSize = 1024 * 1024 * memClass;
    119         mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) {
    120             @Override
    121             protected int sizeOf(String key, BitmapItem bitmap) {
    122                 return bitmap.mBitmap.getByteCount();
    123             }
    124         };
    125     }
    126 
    127     /**
    128      * load bitmap in current thread, will *block* current thread.
    129      * FIXME: Should avoid using this function at all cost.
    130      * @deprecated
    131      */
    132     @Deprecated
    133     public final Bitmap loadBitmapBlocking(BitmapWorkerOptions options) {
    134         final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
    135         Bitmap bitmap = null;
    136         if (hasAccountImageUri) {
    137             AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
    138         } else {
    139             bitmap = getBitmapFromMemCache(options);
    140         }
    141 
    142         if (bitmap == null) {
    143             BitmapWorkerTask task = new BitmapWorkerTask(null) {
    144                 @Override
    145                 protected Bitmap doInBackground(BitmapWorkerOptions... params) {
    146                     final Bitmap bitmap = super.doInBackground(params);
    147                     if (bitmap != null && !hasAccountImageUri) {
    148                         addBitmapToMemoryCache(params[0], bitmap, isScaled());
    149                     }
    150                     return bitmap;
    151                 }
    152             };
    153 
    154             return task.doInBackground(options);
    155         }
    156         return bitmap;
    157     }
    158 
    159     /**
    160      * Loads the bitmap into the image view.
    161      */
    162     public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) {
    163         cancelDownload(imageView);
    164         final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
    165         Bitmap bitmap = null;
    166         if (hasAccountImageUri) {
    167             AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
    168         } else {
    169             bitmap = getBitmapFromMemCache(options);
    170         }
    171 
    172         if (bitmap != null) {
    173             imageView.setImageBitmap(bitmap);
    174         } else {
    175             BitmapWorkerTask task = new BitmapWorkerTask(imageView) {
    176                 @Override
    177                 protected Bitmap doInBackground(BitmapWorkerOptions... params) {
    178                     Bitmap bitmap = super.doInBackground(params);
    179                     if (bitmap != null && !hasAccountImageUri) {
    180                         addBitmapToMemoryCache(params[0], bitmap, isScaled());
    181                     }
    182                     return bitmap;
    183                 }
    184             };
    185             imageView.setTag(R.id.imageDownloadTask, new SoftReference<>(task));
    186             task.execute(options);
    187         }
    188     }
    189 
    190     /**
    191      * Loads the bitmap.
    192      * <p>
    193      * This will be sent back to the callback object.
    194      */
    195     public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) {
    196         cancelDownload(callback);
    197         final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
    198         final Bitmap bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options);
    199         if (hasAccountImageUri) {
    200             AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
    201         }
    202 
    203         BitmapWorkerTask task = new BitmapWorkerTask(null) {
    204             @Override
    205             protected Bitmap doInBackground(BitmapWorkerOptions... params) {
    206                 if (bitmap != null) {
    207                     return bitmap;
    208                 }
    209                 final Bitmap bitmap = super.doInBackground(params);
    210                 if (bitmap != null && !hasAccountImageUri) {
    211                     addBitmapToMemoryCache(params[0], bitmap, isScaled());
    212                 }
    213                 return bitmap;
    214             }
    215 
    216             @Override
    217             protected void onPostExecute(Bitmap bitmap) {
    218                 callback.onBitmapRetrieved(bitmap);
    219             }
    220         };
    221         callback.mTask = new SoftReference<>(task);
    222         task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
    223     }
    224 
    225     /**
    226      * Cancel download<p>
    227      * @param key {@link BitmapCallback} or {@link ImageView}
    228      */
    229     public boolean cancelDownload(Object key) {
    230         BitmapWorkerTask task = null;
    231         if (key instanceof ImageView) {
    232             ImageView imageView = (ImageView)key;
    233             SoftReference<BitmapWorkerTask> softReference =
    234                     (SoftReference<BitmapWorkerTask>) imageView.getTag(R.id.imageDownloadTask);
    235             if (softReference != null) {
    236                 task = softReference.get();
    237                 softReference.clear();
    238             }
    239         } else if (key instanceof BitmapCallback) {
    240             BitmapCallback callback = (BitmapCallback)key;
    241             if (callback.mTask != null) {
    242                 task = callback.mTask.get();
    243                 callback.mTask = null;
    244             }
    245         }
    246         if (task != null) {
    247             return task.cancel(true);
    248         }
    249         return false;
    250     }
    251 
    252     private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig, int width) {
    253         for (int i = 0; i < SIZE_BUCKET.length; i++) {
    254             if (width <= SIZE_BUCKET[i]) {
    255                 return new StringBuilder(baseKey.length() + 16).append(baseKey)
    256                         .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal())
    257                         .append(":").append(SIZE_BUCKET[i]).toString();
    258             }
    259         }
    260         // should never happen because last bucket is Integer.MAX_VALUE
    261         throw new RuntimeException();
    262     }
    263 
    264     private void addBitmapToMemoryCache(BitmapWorkerOptions key, Bitmap bitmap, boolean isScaled) {
    265         if (!key.isMemCacheEnabled()) {
    266             return;
    267         }
    268         String bucketKey = getBucketKey(
    269                 key.getCacheKey(), key.getBitmapConfig(), bitmap.getHeight());
    270         BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
    271         if (bitmapItem != null) {
    272             Bitmap currentBitmap = bitmapItem.mBitmap;
    273             // If somebody else happened to get a larger one in the bucket, discard our bitmap.
    274             // TODO: need a better way to prevent current downloading for the same Bitmap
    275             if (currentBitmap.getWidth() >= bitmap.getWidth() && currentBitmap.getHeight()
    276                     >= bitmap.getHeight()) {
    277                 return;
    278             }
    279         }
    280         if (DEBUG) {
    281             Log.d(TAG, "add cache "+bucketKey+" isScaled = "+isScaled);
    282         }
    283         bitmapItem = new BitmapItem(bitmap, isScaled);
    284         mMemoryCache.put(bucketKey, bitmapItem);
    285     }
    286 
    287     private Bitmap getBitmapFromMemCache(BitmapWorkerOptions key) {
    288         if (key.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
    289             // 1. find the bitmap in the size bucket
    290             String bucketKey =
    291                     getBucketKey(key.getCacheKey(), key.getBitmapConfig(), key.getHeight());
    292             BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
    293             if (bitmapItem != null) {
    294                 Bitmap bitmap = bitmapItem.mBitmap;
    295                 // now we have the bitmap in the bucket, use it when the bitmap is not scaled or
    296                 // if the size is larger than or equals to the output size
    297                 if (!bitmapItem.mScaled) {
    298                     return bitmap;
    299                 }
    300                 if (bitmap.getHeight() >= key.getHeight()) {
    301                     return bitmap;
    302                 }
    303             }
    304             // 2. find un-scaled bitmap in smaller buckets.  If the un-scaled bitmap exists
    305             // in higher buckets,  we still need to scale it down.  Right now we just
    306             // return null and let the BitmapWorkerTask to do the same job again.
    307             // TODO: use the existing unscaled bitmap and we don't need to load it from resource
    308             // or network again.
    309             for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
    310                 if (SIZE_BUCKET[i] >= key.getHeight()) {
    311                     continue;
    312                 }
    313                 bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
    314                 bitmapItem = mMemoryCache.get(bucketKey);
    315                 if (bitmapItem != null && !bitmapItem.mScaled) {
    316                     return bitmapItem.mBitmap;
    317                 }
    318             }
    319             return null;
    320         }
    321         // 3. find un-scaled bitmap if size is not specified
    322         for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
    323             String bucketKey =
    324                     getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
    325             BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
    326             if (bitmapItem != null && !bitmapItem.mScaled) {
    327                 return bitmapItem.mBitmap;
    328             }
    329         }
    330         return null;
    331     }
    332 
    333     public Bitmap getLargestBitmapFromMemCache(BitmapWorkerOptions key) {
    334         // find largest bitmap matching the key
    335         for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
    336             String bucketKey =
    337                     getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
    338             BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
    339             if (bitmapItem != null) {
    340                 return bitmapItem.mBitmap;
    341             }
    342         }
    343         return null;
    344     }
    345 
    346     public void invalidateCachedResources() {
    347         Map<String, BitmapItem> snapshot = mMemoryCache.snapshot();
    348         for (String uri: snapshot.keySet()) {
    349             Log.d(TAG, "remove cached image: " + uri);
    350             if (uri.startsWith(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
    351                 mMemoryCache.remove(uri);
    352             }
    353         }
    354     }
    355 }
    356