• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BitmapItem(Bitmap bitmap, boolean scaled)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 
onBitmapRetrieved(Bitmap bitmap)92         public abstract void onBitmapRetrieved(Bitmap bitmap);
93     }
94 
95     /**
96      * get the singleton BitmapDownloader for the application
97      */
getInstance(Context context)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 
BitmapDownloader(Context context)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
loadBitmapBlocking(BitmapWorkerOptions options)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      */
loadBitmap(BitmapWorkerOptions options, final ImageView imageView)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      */
getBitmap(BitmapWorkerOptions options, final BitmapCallback callback)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      */
cancelDownload(Object key)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 
getBucketKey(String baseKey, Bitmap.Config bitmapConfig, int width)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 
addBitmapToMemoryCache(BitmapWorkerOptions key, Bitmap bitmap, boolean isScaled)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 
getBitmapFromMemCache(BitmapWorkerOptions key)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 
getLargestBitmapFromMemCache(BitmapWorkerOptions key)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