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