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