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.content.Intent.ShortcutIconResource;
22 import android.content.pm.PackageManager.NameNotFoundException;
23 import android.graphics.Bitmap;
24 import android.graphics.drawable.BitmapDrawable;
25 import android.graphics.drawable.Drawable;
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.ArrayList;
36 import java.util.concurrent.Executor;
37 import java.util.concurrent.Executors;
38 
39 /**
40  * Downloader class which loads a resource URI into an image view or triggers a callback
41  * <p>
42  * This class adds a LRU cache over DrawableLoader.
43  * <p>
44  * Calling getBitmap() or loadBitmap() will return a RefcountBitmapDrawable with initial refcount =
45  * 2 by the cache table and by caller.  You must call releaseRef() when you are done with the resource.
46  * The most common way is using RefcountImageView, and releaseRef() for you.  Once both RefcountImageView
47  * and LRUCache removes the refcount, the underlying bitmap will be used for decoding new bitmap.
48  * <p>
49  * If the URI does not point to a bitmap (e.g. point to a drawable xml, we won't cache it and we
50  * directly return a regular Drawable).
51  */
52 public class DrawableDownloader {
53 
54     private static final String TAG = "DrawableDownloader";
55 
56     private static final boolean DEBUG = false;
57 
58     private static final int CORE_POOL_SIZE = 5;
59 
60     // thread pool for loading non android-resources such as http,  content
61     private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR =
62             Executors.newFixedThreadPool(CORE_POOL_SIZE);
63 
64     private static final int CORE_RESOURCE_POOL_SIZE = 1;
65 
66     // thread pool for loading android resources,  we use separate thread pool so
67     // that network loading will not block local android icons
68     private static final Executor BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR =
69             Executors.newFixedThreadPool(CORE_RESOURCE_POOL_SIZE);
70 
71     // 1/4 of max memory is used for bitmap mem cache
72     private static final int MEM_TO_CACHE = 4;
73 
74     // hard limit for bitmap mem cache in MB
75     private static final int CACHE_HARD_LIMIT = 32;
76 
77     /**
78      * bitmap cache item structure saved in LruCache
79      */
80     private static class BitmapItem {
81         final int mOriginalWidth;
82         final int mOriginalHeight;
83         final ArrayList<BitmapDrawable> mBitmaps = new ArrayList<>(3);
84         int mByteCount;
BitmapItem(int originalWidth, int originalHeight)85         public BitmapItem(int originalWidth, int originalHeight) {
86             mOriginalWidth = originalWidth;
87             mOriginalHeight = originalHeight;
88         }
89 
90         // get bitmap from the list
findDrawable(BitmapWorkerOptions options)91         BitmapDrawable findDrawable(BitmapWorkerOptions options) {
92             for (int i = 0, c = mBitmaps.size(); i < c; i++) {
93                 BitmapDrawable d = mBitmaps.get(i);
94                 // use drawable with original size
95                 if (d.getIntrinsicWidth() == mOriginalWidth
96                         && d.getIntrinsicHeight() == mOriginalHeight) {
97                     return d;
98                 }
99                 // if specified width/height in options and is smaller than
100                 // cached one, we can use this cached drawable
101                 if (options.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
102                     if (options.getHeight() <= d.getIntrinsicHeight()) {
103                         return d;
104                     }
105                 } else if (options.getWidth() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
106                     if (options.getWidth() <= d.getIntrinsicWidth()) {
107                         return d;
108                     }
109                 }
110             }
111             return null;
112         }
113 
findLargestDrawable(BitmapWorkerOptions options)114         BitmapDrawable findLargestDrawable(BitmapWorkerOptions options) {
115             return mBitmaps.size() == 0 ? null : mBitmaps.get(0);
116         }
117 
addDrawable(BitmapDrawable d)118         void addDrawable(BitmapDrawable d) {
119             int i = 0, c = mBitmaps.size();
120             for (; i < c; i++) {
121                 BitmapDrawable drawable = mBitmaps.get(i);
122                 if (drawable.getIntrinsicHeight() < d.getIntrinsicHeight()) {
123                     break;
124                 }
125             }
126             mBitmaps.add(i, d);
127             mByteCount += RecycleBitmapPool.getSize(d.getBitmap());
128         }
129 
clear()130         void clear() {
131             for (int i = 0, c = mBitmaps.size(); i < c; i++) {
132                 BitmapDrawable d = mBitmaps.get(i);
133                 if (d instanceof RefcountBitmapDrawable) {
134                     ((RefcountBitmapDrawable) d).getRefcountObject().releaseRef();
135                 }
136             }
137             mBitmaps.clear();
138             mByteCount = 0;
139         }
140     }
141 
142     public static abstract class BitmapCallback {
143         SoftReference<DrawableLoader> mTask;
144 
onBitmapRetrieved(Drawable bitmap)145         public abstract void onBitmapRetrieved(Drawable bitmap);
146     }
147 
148     private final Context mContext;
149     private final LruCache<String, BitmapItem> mMemoryCache;
150     private final RecycleBitmapPool mRecycledBitmaps;
151 
152     private static DrawableDownloader sBitmapDownloader;
153 
154     private static final Object sBitmapDownloaderLock = new Object();
155 
156     /**
157      * get the singleton BitmapDownloader for the application
158      */
getInstance(Context context)159     public final static DrawableDownloader getInstance(Context context) {
160         if (sBitmapDownloader == null) {
161             synchronized(sBitmapDownloaderLock) {
162                 if (sBitmapDownloader == null) {
163                     sBitmapDownloader = new DrawableDownloader(context);
164                 }
165             }
166         }
167         return sBitmapDownloader;
168     }
169 
getBucketKey(String baseKey, Bitmap.Config bitmapConfig)170     private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig) {
171         return new StringBuilder(baseKey.length() + 16).append(baseKey)
172                          .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal())
173                          .toString();
174      }
175 
getDrawable(Context context, ShortcutIconResource iconResource)176     public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
177             throws NameNotFoundException {
178         return DrawableLoader.getDrawable(context, iconResource);
179     }
180 
DrawableDownloader(Context context)181     private DrawableDownloader(Context context) {
182         mContext = context;
183         int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
184                 .getMemoryClass();
185         memClass = memClass / MEM_TO_CACHE;
186         if (memClass > CACHE_HARD_LIMIT) {
187             memClass = CACHE_HARD_LIMIT;
188         }
189         int cacheSize = 1024 * 1024 * memClass;
190         mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) {
191             @Override
192             protected int sizeOf(String key, BitmapItem bitmap) {
193                 return bitmap.mByteCount;
194             }
195             @Override
196             protected void entryRemoved(
197                     boolean evicted, String key, BitmapItem oldValue, BitmapItem newValue) {
198                 if (evicted) {
199                     oldValue.clear();
200                 }
201             }
202         };
203         mRecycledBitmaps = new RecycleBitmapPool();
204     }
205 
206     /**
207      * trim memory cache to 0~1 * maxSize
208      */
trimTo(float amount)209     public void trimTo(float amount) {
210         if (amount == 0f) {
211             mMemoryCache.evictAll();
212         } else {
213             mMemoryCache.trimToSize((int) (amount * mMemoryCache.maxSize()));
214         }
215     }
216 
217     /**
218      * load bitmap in current thread, will *block* current thread.
219      * FIXME: Should avoid using this function at all cost.
220      * @deprecated
221      */
222     @Deprecated
loadBitmapBlocking(BitmapWorkerOptions options)223     public final Drawable loadBitmapBlocking(BitmapWorkerOptions options) {
224         final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
225         Drawable bitmap = null;
226         if (hasAccountImageUri) {
227             AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
228         } else {
229             bitmap = getBitmapFromMemCache(options);
230         }
231 
232         if (bitmap == null) {
233             DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) {
234                 @Override
235                 protected Drawable doInBackground(BitmapWorkerOptions... params) {
236                     final Drawable bitmap = super.doInBackground(params);
237                     if (bitmap != null && !hasAccountImageUri) {
238                         addBitmapToMemoryCache(params[0], bitmap, this);
239                     }
240                     return bitmap;
241                 }
242             };
243             return task.doInBackground(options);
244         }
245         return bitmap;
246     }
247 
248     /**
249      * Loads the bitmap into the image view.
250      */
loadBitmap(BitmapWorkerOptions options, final ImageView imageView)251     public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) {
252         cancelDownload(imageView);
253         final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
254         Drawable bitmap = null;
255         if (hasAccountImageUri) {
256             AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
257         } else {
258             bitmap = getBitmapFromMemCache(options);
259         }
260 
261         if (bitmap != null) {
262             imageView.setImageDrawable(bitmap);
263         } else {
264             DrawableLoader task = new DrawableLoader(imageView, mRecycledBitmaps) {
265                 @Override
266                 protected Drawable doInBackground(BitmapWorkerOptions... params) {
267                     Drawable bitmap = super.doInBackground(params);
268                     if (bitmap != null && !hasAccountImageUri) {
269                         addBitmapToMemoryCache(params[0], bitmap, this);
270                     }
271                     return bitmap;
272                 }
273             };
274             imageView.setTag(R.id.imageDownloadTask, new SoftReference<DrawableLoader>(task));
275             scheduleTask(task, options);
276         }
277     }
278 
279     /**
280      * Loads the bitmap.
281      * <p>
282      * This will be sent back to the callback object.
283      */
getBitmap(BitmapWorkerOptions options, final BitmapCallback callback)284     public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) {
285         cancelDownload(callback);
286         final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
287         final Drawable bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options);
288         if (hasAccountImageUri) {
289             AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
290         }
291 
292         if (bitmap != null) {
293             callback.onBitmapRetrieved(bitmap);
294             return;
295         }
296         DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) {
297             @Override
298             protected Drawable doInBackground(BitmapWorkerOptions... params) {
299                 final Drawable bitmap = super.doInBackground(params);
300                 if (bitmap != null && !hasAccountImageUri) {
301                     addBitmapToMemoryCache(params[0], bitmap, this);
302                 }
303                 return bitmap;
304             }
305 
306             @Override
307             protected void onPostExecute(Drawable bitmap) {
308                 callback.onBitmapRetrieved(bitmap);
309             }
310         };
311         callback.mTask = new SoftReference<DrawableLoader>(task);
312         scheduleTask(task, options);
313     }
314 
scheduleTask(DrawableLoader task, BitmapWorkerOptions options)315     private static void scheduleTask(DrawableLoader task, BitmapWorkerOptions options) {
316         if (options.isFromResource()) {
317             task.executeOnExecutor(BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
318         } else {
319             task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
320         }
321     }
322 
323     /**
324      * Cancel download<p>
325      * @param key {@link BitmapCallback} or {@link ImageView}
326      */
cancelDownload(Object key)327     public boolean cancelDownload(Object key) {
328         DrawableLoader task = null;
329         if (key instanceof ImageView) {
330             ImageView imageView = (ImageView)key;
331             SoftReference<DrawableLoader> softReference =
332                     (SoftReference<DrawableLoader>) imageView.getTag(R.id.imageDownloadTask);
333             if (softReference != null) {
334                 task = softReference.get();
335                 softReference.clear();
336             }
337         } else if (key instanceof BitmapCallback) {
338             BitmapCallback callback = (BitmapCallback)key;
339             if (callback.mTask != null) {
340                 task = callback.mTask.get();
341                 callback.mTask = null;
342             }
343         }
344         if (task != null) {
345             return task.cancel(true);
346         }
347         return false;
348     }
349 
addBitmapToMemoryCache(BitmapWorkerOptions key, Drawable bitmap, DrawableLoader loader)350     private void addBitmapToMemoryCache(BitmapWorkerOptions key, Drawable bitmap,
351             DrawableLoader loader) {
352         if (!key.isMemCacheEnabled()) {
353             return;
354         }
355         if (!(bitmap instanceof BitmapDrawable)) {
356             return;
357         }
358         String bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig());
359         BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
360         if (DEBUG) {
361             Log.d(TAG, "add cache "+bucketKey);
362         }
363         if (bitmapItem != null) {
364             // remove and re-add to update size
365             mMemoryCache.remove(bucketKey);
366         } else {
367             bitmapItem = new BitmapItem(loader.getOriginalWidth(), loader.getOriginalHeight());
368         }
369         if (bitmap instanceof RefcountBitmapDrawable) {
370             RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) bitmap;
371             refcountDrawable.getRefcountObject().addRef();
372         }
373         bitmapItem.addDrawable((BitmapDrawable) bitmap);
374         mMemoryCache.put(bucketKey, bitmapItem);
375     }
376 
getBitmapFromMemCache(BitmapWorkerOptions key)377     private Drawable getBitmapFromMemCache(BitmapWorkerOptions key) {
378         String bucketKey =
379                 getBucketKey(key.getCacheKey(), key.getBitmapConfig());
380         BitmapItem item = mMemoryCache.get(bucketKey);
381         if (item != null) {
382             return createRefCopy(item.findDrawable(key));
383         }
384         return null;
385     }
386 
getLargestBitmapFromMemCache(BitmapWorkerOptions key)387     public BitmapDrawable getLargestBitmapFromMemCache(BitmapWorkerOptions key) {
388         String bucketKey =
389                 getBucketKey(key.getCacheKey(), key.getBitmapConfig());
390         BitmapItem item = mMemoryCache.get(bucketKey);
391         if (item != null) {
392             return (BitmapDrawable) createRefCopy(item.findLargestDrawable(key));
393         }
394         return null;
395     }
396 
createRefCopy(Drawable d)397     private Drawable createRefCopy(Drawable d) {
398         if (d != null) {
399             if (d instanceof RefcountBitmapDrawable) {
400                 RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) d;
401                 refcountDrawable.getRefcountObject().addRef();
402                 d = new RefcountBitmapDrawable(mContext.getResources(),
403                         refcountDrawable);
404             }
405             return d;
406         }
407         return null;
408     }
409 
410 }
411