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