• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.util;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.drawable.BitmapDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.media.tv.TvInputInfo;
24 import android.os.AsyncTask;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.support.annotation.MainThread;
28 import android.support.annotation.Nullable;
29 import android.support.annotation.UiThread;
30 import android.support.annotation.WorkerThread;
31 import android.util.ArraySet;
32 import android.util.Log;
33 
34 import com.android.tv.R;
35 import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
36 
37 import java.lang.ref.WeakReference;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.Set;
41 import java.util.concurrent.BlockingQueue;
42 import java.util.concurrent.Executor;
43 import java.util.concurrent.LinkedBlockingQueue;
44 import java.util.concurrent.RejectedExecutionException;
45 import java.util.concurrent.ThreadFactory;
46 import java.util.concurrent.ThreadPoolExecutor;
47 import java.util.concurrent.TimeUnit;
48 
49 /**
50  * This class wraps up completing some arbitrary long running work when loading a bitmap. It
51  * handles things like using a memory cache, running the work in a background thread.
52  */
53 public final class ImageLoader {
54     private static final String TAG = "ImageLoader";
55     private static final boolean DEBUG = false;
56 
57     private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
58     // We want at least 2 threads and at most 4 threads in the core pool,
59     // preferring to have 1 less than the CPU count to avoid saturating
60     // the CPU with background work
61     private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
62     private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
63     private static final int KEEP_ALIVE_SECONDS = 30;
64 
65     private static final ThreadFactory sThreadFactory = new NamedThreadFactory("ImageLoader");
66 
67     private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<>(128);
68 
69     /**
70      * An private {@link Executor} that can be used to execute tasks in parallel.
71      *
72      * <p>{@code IMAGE_THREAD_POOL_EXECUTOR} setting are copied from {@link AsyncTask}
73      * Since we do a lot of concurrent image loading we can exhaust a thread pool.
74      * ImageLoader catches the error, and just leaves the image blank.
75      * However other tasks will fail and crash the application.
76      *
77      * <p>Using a separate thread pool prevents image loading from causing other tasks to fail.
78      */
79     private static final Executor IMAGE_THREAD_POOL_EXECUTOR;
80 
81     static {
82         ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
83                 MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue,
84                 sThreadFactory);
85         threadPoolExecutor.allowCoreThreadTimeOut(true);
86         IMAGE_THREAD_POOL_EXECUTOR = threadPoolExecutor;
87     }
88 
89     private static Handler sMainHandler;
90 
91     /**
92      * Handles when image loading is finished.
93      *
94      * <p>Use this to prevent leaking an Activity or other Context while image loading is
95      *  still pending. When you extend this class you <strong>MUST NOT</strong> use a non static
96      *  inner class, or the containing object will still be leaked.
97      */
98     @UiThread
99     public static abstract class ImageLoaderCallback<T> {
100         private final WeakReference<T> mWeakReference;
101 
102         /**
103          * Creates an callback keeping a weak reference to {@code referent}.
104          *
105          * <p> If the "referent" is no longer valid, it no longer makes sense to run the
106          * callback. The referent is the View, or Activity or whatever that actually needs to
107          * receive the Bitmap.  If the referent has been GC, then no need to run the callback.
108          */
ImageLoaderCallback(T referent)109         public ImageLoaderCallback(T referent) {
110             mWeakReference = new WeakReference<>(referent);
111         }
112 
113         /**
114          * Called when bitmap is loaded.
115          */
onBitmapLoaded(@ullable Bitmap bitmap)116         private void onBitmapLoaded(@Nullable Bitmap bitmap) {
117             T referent = mWeakReference.get();
118             if (referent != null) {
119                 onBitmapLoaded(referent, bitmap);
120             } else {
121                 if (DEBUG) Log.d(TAG, "onBitmapLoaded not called because weak reference is gone");
122             }
123         }
124 
125         /**
126          * Called when bitmap is loaded if the weak reference is still valid.
127          */
onBitmapLoaded(T referent, @Nullable Bitmap bitmap)128         public abstract void onBitmapLoaded(T referent, @Nullable Bitmap bitmap);
129     }
130 
131     private static final Map<String, LoadBitmapTask> sPendingListMap = new HashMap<>();
132 
133     /**
134      * Preload a bitmap image into the cache.
135      *
136      * <p>Not to make heavy CPU load, AsyncTask.SERIAL_EXECUTOR is used for the image loading.
137      * <p>This method is thread safe.
138      */
prefetchBitmap(Context context, final String uriString, final int maxWidth, final int maxHeight)139     public static void prefetchBitmap(Context context, final String uriString, final int maxWidth,
140             final int maxHeight) {
141         if (DEBUG) Log.d(TAG, "prefetchBitmap() " + uriString);
142         if (Looper.getMainLooper() == Looper.myLooper()) {
143             doLoadBitmap(context, uriString, maxWidth, maxHeight, null, AsyncTask.SERIAL_EXECUTOR);
144         } else {
145             final Context appContext = context.getApplicationContext();
146             getMainHandler().post(new Runnable() {
147                 @Override
148                 @MainThread
149                 public void run() {
150                     // Calling from the main thread prevents a ConcurrentModificationException
151                     // in LoadBitmapTask.onPostExecute
152                     doLoadBitmap(appContext, uriString, maxWidth, maxHeight, null,
153                             AsyncTask.SERIAL_EXECUTOR);
154                 }
155             });
156         }
157     }
158 
159     /**
160      * Load a bitmap image with the cache using a ContentResolver.
161      *
162      * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in
163      * the cache.
164      *
165      * @return {@code true} if the load is complete and the callback is executed.
166      */
167     @UiThread
loadBitmap(Context context, String uriString, ImageLoaderCallback callback)168     public static boolean loadBitmap(Context context, String uriString,
169             ImageLoaderCallback callback) {
170         return loadBitmap(context, uriString, Integer.MAX_VALUE, Integer.MAX_VALUE, callback);
171     }
172 
173     /**
174      * Load a bitmap image with the cache and resize it with given params.
175      *
176      * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in
177      * the cache.
178      *
179      * @return {@code true} if the load is complete and the callback is executed.
180      */
181     @UiThread
loadBitmap(Context context, String uriString, int maxWidth, int maxHeight, ImageLoaderCallback callback)182     public static boolean loadBitmap(Context context, String uriString, int maxWidth, int maxHeight,
183             ImageLoaderCallback callback) {
184         if (DEBUG) {
185             Log.d(TAG, "loadBitmap() " + uriString);
186         }
187         return doLoadBitmap(context, uriString, maxWidth, maxHeight, callback,
188                 IMAGE_THREAD_POOL_EXECUTOR);
189     }
190 
doLoadBitmap(Context context, String uriString, int maxWidth, int maxHeight, ImageLoaderCallback callback, Executor executor)191     private static boolean doLoadBitmap(Context context, String uriString,
192             int maxWidth, int maxHeight, ImageLoaderCallback callback, Executor executor) {
193         // Check the cache before creating a Task.  The cache will be checked again in doLoadBitmap
194         // but checking a cache is much cheaper than creating an new task.
195         ImageCache imageCache = ImageCache.getInstance();
196         ScaledBitmapInfo bitmapInfo = imageCache.get(uriString);
197         if (bitmapInfo != null && !bitmapInfo.needToReload(maxWidth, maxHeight)) {
198             if (callback != null) {
199                 callback.onBitmapLoaded(bitmapInfo.bitmap);
200             }
201             return true;
202         }
203         return doLoadBitmap(callback, executor,
204                 new LoadBitmapFromUriTask(context, imageCache, uriString, maxWidth, maxHeight));
205     }
206 
207     /**
208      * Load a bitmap image with the cache and resize it with given params.
209      *
210      * <p>The LoadBitmapTask will be executed on a non ui thread.
211      *
212      * @return {@code true} if the load is complete and the callback is executed.
213      */
214     @UiThread
loadBitmap(ImageLoaderCallback callback, LoadBitmapTask loadBitmapTask)215     public static boolean loadBitmap(ImageLoaderCallback callback, LoadBitmapTask loadBitmapTask) {
216         if (DEBUG) {
217             Log.d(TAG, "loadBitmap() " + loadBitmapTask);
218         }
219         return doLoadBitmap(callback, IMAGE_THREAD_POOL_EXECUTOR, loadBitmapTask);
220     }
221 
222     /**
223      * @return {@code true} if the load is complete and the callback is executed.
224      */
225     @UiThread
doLoadBitmap(ImageLoaderCallback callback, Executor executor, LoadBitmapTask loadBitmapTask)226     private static boolean doLoadBitmap(ImageLoaderCallback callback, Executor executor,
227             LoadBitmapTask loadBitmapTask) {
228         ScaledBitmapInfo bitmapInfo = loadBitmapTask.getFromCache();
229         boolean needToReload = loadBitmapTask.isReloadNeeded();
230         if (bitmapInfo != null && !needToReload) {
231             if (callback != null) {
232                 callback.onBitmapLoaded(bitmapInfo.bitmap);
233             }
234             return true;
235         }
236         LoadBitmapTask existingTask = sPendingListMap.get(loadBitmapTask.getKey());
237         if (existingTask != null && !loadBitmapTask.isReloadNeeded(existingTask)) {
238             // The image loading is already scheduled and is large enough.
239             if (callback != null) {
240                 existingTask.mCallbacks.add(callback);
241             }
242         } else {
243             if (callback != null) {
244                 loadBitmapTask.mCallbacks.add(callback);
245             }
246             sPendingListMap.put(loadBitmapTask.getKey(), loadBitmapTask);
247             try {
248                 loadBitmapTask.executeOnExecutor(executor);
249             } catch (RejectedExecutionException e) {
250                 Log.e(TAG, "Failed to create new image loader", e);
251                 sPendingListMap.remove(loadBitmapTask.getKey());
252             }
253         }
254         return false;
255     }
256 
257     /**
258      * Loads and caches a a possibly scaled down version of a bitmap.
259      *
260      * <p>Implement {@link #doGetBitmapInBackground} to do the actual loading.
261      */
262     public static abstract class LoadBitmapTask extends AsyncTask<Void, Void, ScaledBitmapInfo> {
263         protected final Context mAppContext;
264         protected final int mMaxWidth;
265         protected final int mMaxHeight;
266         private final Set<ImageLoaderCallback> mCallbacks = new ArraySet<>();
267         private final ImageCache mImageCache;
268         private final String mKey;
269 
270         /**
271          * Returns true if a reload is needed compared to current results in the cache or false if
272          * there is not match in the cache.
273          */
isReloadNeeded()274         private boolean isReloadNeeded() {
275             ScaledBitmapInfo bitmapInfo = getFromCache();
276             boolean needToReload = bitmapInfo != null && bitmapInfo
277                     .needToReload(mMaxWidth, mMaxHeight);
278             if (DEBUG) {
279                 if (needToReload) {
280                     Log.d(TAG, "Bitmap needs to be reloaded. {"
281                             + "originalWidth=" + bitmapInfo.bitmap.getWidth()
282                             + ", originalHeight=" + bitmapInfo.bitmap.getHeight()
283                             + ", reqWidth=" + mMaxWidth
284                             + ", reqHeight=" + mMaxHeight
285                             + "}");
286                 }
287             }
288             return needToReload;
289         }
290 
291         /**
292          * Checks if a reload would be needed if the results of other was available.
293          */
isReloadNeeded(LoadBitmapTask other)294         private boolean isReloadNeeded(LoadBitmapTask other) {
295             return mMaxHeight >= other.mMaxHeight * 2 || mMaxWidth >= other.mMaxWidth * 2;
296         }
297 
298         @Nullable
getFromCache()299         public final ScaledBitmapInfo getFromCache() {
300             return mImageCache.get(mKey);
301         }
302 
LoadBitmapTask(Context context, ImageCache imageCache, String key, int maxHeight, int maxWidth)303         public LoadBitmapTask(Context context, ImageCache imageCache, String key, int maxHeight,
304                 int maxWidth) {
305             if (maxWidth == 0 || maxHeight == 0) {
306                 throw new IllegalArgumentException(
307                         "Image size should not be 0. {width=" + maxWidth + ", height=" + maxHeight
308                                 + "}");
309             }
310             mAppContext = context.getApplicationContext();
311             mKey = key;
312             mImageCache = imageCache;
313             mMaxHeight = maxHeight;
314             mMaxWidth = maxWidth;
315         }
316 
317         /**
318          * Loads the bitmap returning a possibly scaled down version.
319          */
320         @Nullable
321         @WorkerThread
doGetBitmapInBackground()322         public abstract ScaledBitmapInfo doGetBitmapInBackground();
323 
324         @Override
325         @Nullable
doInBackground(Void... params)326         public final ScaledBitmapInfo doInBackground(Void... params) {
327             ScaledBitmapInfo bitmapInfo = getFromCache();
328             if (bitmapInfo != null && !isReloadNeeded()) {
329                 return bitmapInfo;
330             }
331             bitmapInfo = doGetBitmapInBackground();
332             if (bitmapInfo != null) {
333                 mImageCache.putIfNeeded(bitmapInfo);
334             }
335             return bitmapInfo;
336         }
337 
338         @Override
onPostExecute(ScaledBitmapInfo scaledBitmapInfo)339         public final void onPostExecute(ScaledBitmapInfo scaledBitmapInfo) {
340             if (DEBUG) Log.d(ImageLoader.TAG, "Bitmap is loaded " + mKey);
341 
342             for (ImageLoader.ImageLoaderCallback callback : mCallbacks) {
343                 callback.onBitmapLoaded(scaledBitmapInfo == null ? null : scaledBitmapInfo.bitmap);
344             }
345             ImageLoader.sPendingListMap.remove(mKey);
346         }
347 
getKey()348         public final String getKey() {
349             return mKey;
350         }
351 
352         @Override
toString()353         public String toString() {
354             return this.getClass().getSimpleName() + "(" + mKey + " " + mMaxWidth + "x" + mMaxHeight
355                     + ")";
356         }
357     }
358 
359     private static final class LoadBitmapFromUriTask extends LoadBitmapTask {
LoadBitmapFromUriTask(Context context, ImageCache imageCache, String uriString, int maxWidth, int maxHeight)360         private LoadBitmapFromUriTask(Context context, ImageCache imageCache, String uriString,
361                 int maxWidth, int maxHeight) {
362             super(context, imageCache, uriString, maxHeight, maxWidth);
363         }
364 
365         @Override
366         @Nullable
doGetBitmapInBackground()367         public final ScaledBitmapInfo doGetBitmapInBackground() {
368             return BitmapUtils
369                     .decodeSampledBitmapFromUriString(mAppContext, getKey(), mMaxWidth, mMaxHeight);
370         }
371     }
372 
373     /**
374      * Loads and caches the logo for a given {@link TvInputInfo}
375      */
376     public static final class LoadTvInputLogoTask extends LoadBitmapTask {
377         private final TvInputInfo mInfo;
378 
LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info)379         public LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info) {
380             super(context,
381                     cache,
382                     getTvInputLogoKey(info.getId()),
383                     context.getResources()
384                             .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size),
385                     context.getResources()
386                             .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size)
387             );
388             mInfo = info;
389         }
390 
391         @Nullable
392         @Override
doGetBitmapInBackground()393         public ScaledBitmapInfo doGetBitmapInBackground() {
394             Drawable drawable = mInfo.loadIcon(mAppContext);
395             if (!(drawable instanceof BitmapDrawable)) {
396                 return null;
397             }
398             Bitmap original = ((BitmapDrawable) drawable).getBitmap();
399             if (original == null) {
400                 return null;
401             }
402             return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight);
403         }
404 
405         /**
406          * Returns key of TV input logo.
407          */
getTvInputLogoKey(String inputId)408         public static String getTvInputLogoKey(String inputId) {
409             return inputId + "-logo";
410         }
411     }
412 
getMainHandler()413     private static synchronized Handler getMainHandler() {
414         if (sMainHandler == null) {
415             sMainHandler = new Handler(Looper.getMainLooper());
416         }
417         return sMainHandler;
418     }
419 
ImageLoader()420     private ImageLoader() {
421     }
422 }
423