• 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.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 
BitmapItem(Bitmap bitmap, boolean scaled)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 
onBitmapRetrieved(Bitmap bitmap)99         public abstract void onBitmapRetrieved(Bitmap bitmap);
100     }
101 
102     /**
103      * get the singleton BitmapDownloader for the application
104      */
getInstance(Context context)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 
BitmapDownloader(Context context)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
loadBitmapBlocking(BitmapWorkerOptions options)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      */
loadBitmap(BitmapWorkerOptions options, final ImageView imageView)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      */
getBitmap(BitmapWorkerOptions options, final BitmapCallback callback)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      */
cancelDownload(Object key)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 
getBucketKey(String baseKey, Bitmap.Config bitmapConfig, int width)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 
addBitmapToMemoryCache(BitmapWorkerOptions key, Bitmap bitmap, boolean isScaled)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 
getBitmapFromMemCache(BitmapWorkerOptions key)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 
getLargestBitmapFromMemCache(BitmapWorkerOptions key)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 
invalidateCachedResources()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