• 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 (other.mMaxHeight != Integer.MAX_VALUE && mMaxHeight >= other.mMaxHeight * 2)
296                     || (other.mMaxWidth != Integer.MAX_VALUE && mMaxWidth >= other.mMaxWidth * 2);
297         }
298 
299         @Nullable
getFromCache()300         public final ScaledBitmapInfo getFromCache() {
301             return mImageCache.get(mKey);
302         }
303 
LoadBitmapTask(Context context, ImageCache imageCache, String key, int maxHeight, int maxWidth)304         public LoadBitmapTask(Context context, ImageCache imageCache, String key, int maxHeight,
305                 int maxWidth) {
306             if (maxWidth == 0 || maxHeight == 0) {
307                 throw new IllegalArgumentException(
308                         "Image size should not be 0. {width=" + maxWidth + ", height=" + maxHeight
309                                 + "}");
310             }
311             mAppContext = context.getApplicationContext();
312             mKey = key;
313             mImageCache = imageCache;
314             mMaxHeight = maxHeight;
315             mMaxWidth = maxWidth;
316         }
317 
318         /**
319          * Loads the bitmap returning a possibly scaled down version.
320          */
321         @Nullable
322         @WorkerThread
doGetBitmapInBackground()323         public abstract ScaledBitmapInfo doGetBitmapInBackground();
324 
325         @Override
326         @Nullable
doInBackground(Void... params)327         public final ScaledBitmapInfo doInBackground(Void... params) {
328             ScaledBitmapInfo bitmapInfo = getFromCache();
329             if (bitmapInfo != null && !isReloadNeeded()) {
330                 return bitmapInfo;
331             }
332             bitmapInfo = doGetBitmapInBackground();
333             if (bitmapInfo != null) {
334                 mImageCache.putIfNeeded(bitmapInfo);
335             }
336             return bitmapInfo;
337         }
338 
339         @Override
onPostExecute(ScaledBitmapInfo scaledBitmapInfo)340         public final void onPostExecute(ScaledBitmapInfo scaledBitmapInfo) {
341             if (DEBUG) Log.d(ImageLoader.TAG, "Bitmap is loaded " + mKey);
342 
343             for (ImageLoader.ImageLoaderCallback callback : mCallbacks) {
344                 callback.onBitmapLoaded(scaledBitmapInfo == null ? null : scaledBitmapInfo.bitmap);
345             }
346             ImageLoader.sPendingListMap.remove(mKey);
347         }
348 
getKey()349         public final String getKey() {
350             return mKey;
351         }
352 
353         @Override
toString()354         public String toString() {
355             return this.getClass().getSimpleName() + "(" + mKey + " " + mMaxWidth + "x" + mMaxHeight
356                     + ")";
357         }
358     }
359 
360     private static final class LoadBitmapFromUriTask extends LoadBitmapTask {
LoadBitmapFromUriTask(Context context, ImageCache imageCache, String uriString, int maxWidth, int maxHeight)361         private LoadBitmapFromUriTask(Context context, ImageCache imageCache, String uriString,
362                 int maxWidth, int maxHeight) {
363             super(context, imageCache, uriString, maxHeight, maxWidth);
364         }
365 
366         @Override
367         @Nullable
doGetBitmapInBackground()368         public final ScaledBitmapInfo doGetBitmapInBackground() {
369             return BitmapUtils
370                     .decodeSampledBitmapFromUriString(mAppContext, getKey(), mMaxWidth, mMaxHeight);
371         }
372     }
373 
374     /**
375      * Loads and caches the logo for a given {@link TvInputInfo}
376      */
377     public static final class LoadTvInputLogoTask extends LoadBitmapTask {
378         private final TvInputInfo mInfo;
379 
LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info)380         public LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info) {
381             super(context,
382                     cache,
383                     getTvInputLogoKey(info.getId()),
384                     context.getResources()
385                             .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size),
386                     context.getResources()
387                             .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size)
388             );
389             mInfo = info;
390         }
391 
392         @Nullable
393         @Override
doGetBitmapInBackground()394         public ScaledBitmapInfo doGetBitmapInBackground() {
395             Drawable drawable = mInfo.loadIcon(mAppContext);
396             if (!(drawable instanceof BitmapDrawable)) {
397                 return null;
398             }
399             Bitmap original = ((BitmapDrawable) drawable).getBitmap();
400             if (original == null) {
401                 return null;
402             }
403             return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight);
404         }
405 
406         /**
407          * Returns key of TV input logo.
408          */
getTvInputLogoKey(String inputId)409         public static String getTvInputLogoKey(String inputId) {
410             return inputId + "-logo";
411         }
412     }
413 
getMainHandler()414     private static synchronized Handler getMainHandler() {
415         if (sMainHandler == null) {
416             sMainHandler = new Handler(Looper.getMainLooper());
417         }
418         return sMainHandler;
419     }
420 
ImageLoader()421     private ImageLoader() {
422     }
423 }
424