aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/util/images/ImageLoader.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/util/images/ImageLoader.java')
-rw-r--r--src/com/android/tv/util/images/ImageLoader.java450
1 files changed, 450 insertions, 0 deletions
diff --git a/src/com/android/tv/util/images/ImageLoader.java b/src/com/android/tv/util/images/ImageLoader.java
new file mode 100644
index 00000000..e844e2ca
--- /dev/null
+++ b/src/com/android/tv/util/images/ImageLoader.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util.images;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.tv.TvInputInfo;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.annotation.WorkerThread;
+import android.util.ArraySet;
+import android.util.Log;
+import com.android.tv.R;
+import com.android.tv.common.concurrent.NamedThreadFactory;
+import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class wraps up completing some arbitrary long running work when loading a bitmap. It handles
+ * things like using a memory cache, running the work in a background thread.
+ */
+public final class ImageLoader {
+ private static final String TAG = "ImageLoader";
+ private static final boolean DEBUG = false;
+
+ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+ // We want at least 2 threads and at most 4 threads in the core pool,
+ // preferring to have 1 less than the CPU count to avoid saturating
+ // the CPU with background work
+ private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
+ private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+ private static final int KEEP_ALIVE_SECONDS = 30;
+
+ private static final ThreadFactory sThreadFactory = new NamedThreadFactory("ImageLoader");
+
+ private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<>(128);
+
+ /**
+ * An private {@link Executor} that can be used to execute tasks in parallel.
+ *
+ * <p>{@code IMAGE_THREAD_POOL_EXECUTOR} setting are copied from {@link AsyncTask} Since we do a
+ * lot of concurrent image loading we can exhaust a thread pool. ImageLoader catches the error,
+ * and just leaves the image blank. However other tasks will fail and crash the application.
+ *
+ * <p>Using a separate thread pool prevents image loading from causing other tasks to fail.
+ */
+ private static final Executor IMAGE_THREAD_POOL_EXECUTOR;
+
+ static {
+ ThreadPoolExecutor threadPoolExecutor =
+ new ThreadPoolExecutor(
+ CORE_POOL_SIZE,
+ MAXIMUM_POOL_SIZE,
+ KEEP_ALIVE_SECONDS,
+ TimeUnit.SECONDS,
+ sPoolWorkQueue,
+ sThreadFactory);
+ threadPoolExecutor.allowCoreThreadTimeOut(true);
+ IMAGE_THREAD_POOL_EXECUTOR = threadPoolExecutor;
+ }
+
+ private static Handler sMainHandler;
+
+ /**
+ * Handles when image loading is finished.
+ *
+ * <p>Use this to prevent leaking an Activity or other Context while image loading is still
+ * pending. When you extend this class you <strong>MUST NOT</strong> use a non static inner
+ * class, or the containing object will still be leaked.
+ */
+ @UiThread
+ public abstract static class ImageLoaderCallback<T> {
+ private final WeakReference<T> mWeakReference;
+
+ /**
+ * Creates an callback keeping a weak reference to {@code referent}.
+ *
+ * <p>If the "referent" is no longer valid, it no longer makes sense to run the callback.
+ * The referent is the View, or Activity or whatever that actually needs to receive the
+ * Bitmap. If the referent has been GC, then no need to run the callback.
+ */
+ public ImageLoaderCallback(T referent) {
+ mWeakReference = new WeakReference<>(referent);
+ }
+
+ /** Called when bitmap is loaded. */
+ private void onBitmapLoaded(@Nullable Bitmap bitmap) {
+ T referent = mWeakReference.get();
+ if (referent != null) {
+ onBitmapLoaded(referent, bitmap);
+ } else {
+ if (DEBUG) Log.d(TAG, "onBitmapLoaded not called because weak reference is gone");
+ }
+ }
+
+ /** Called when bitmap is loaded if the weak reference is still valid. */
+ public abstract void onBitmapLoaded(T referent, @Nullable Bitmap bitmap);
+ }
+
+ private static final Map<String, LoadBitmapTask> sPendingListMap = new HashMap<>();
+
+ /**
+ * Preload a bitmap image into the cache.
+ *
+ * <p>Not to make heavy CPU load, AsyncTask.SERIAL_EXECUTOR is used for the image loading.
+ *
+ * <p>This method is thread safe.
+ */
+ public static void prefetchBitmap(
+ Context context, final String uriString, final int maxWidth, final int maxHeight) {
+ if (DEBUG) Log.d(TAG, "prefetchBitmap() " + uriString);
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ doLoadBitmap(context, uriString, maxWidth, maxHeight, null, AsyncTask.SERIAL_EXECUTOR);
+ } else {
+ final Context appContext = context.getApplicationContext();
+ getMainHandler()
+ .post(
+ new Runnable() {
+ @Override
+ @MainThread
+ public void run() {
+ // Calling from the main thread prevents a
+ // ConcurrentModificationException
+ // in LoadBitmapTask.onPostExecute
+ doLoadBitmap(
+ appContext,
+ uriString,
+ maxWidth,
+ maxHeight,
+ null,
+ AsyncTask.SERIAL_EXECUTOR);
+ }
+ });
+ }
+ }
+
+ /**
+ * Load a bitmap image with the cache using a ContentResolver.
+ *
+ * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in the
+ * cache.
+ *
+ * @return {@code true} if the load is complete and the callback is executed.
+ */
+ @UiThread
+ public static boolean loadBitmap(
+ Context context, String uriString, ImageLoaderCallback callback) {
+ return loadBitmap(context, uriString, Integer.MAX_VALUE, Integer.MAX_VALUE, callback);
+ }
+
+ /**
+ * Load a bitmap image with the cache and resize it with given params.
+ *
+ * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in the
+ * cache.
+ *
+ * @return {@code true} if the load is complete and the callback is executed.
+ */
+ @UiThread
+ public static boolean loadBitmap(
+ Context context,
+ String uriString,
+ int maxWidth,
+ int maxHeight,
+ ImageLoaderCallback callback) {
+ if (DEBUG) {
+ Log.d(TAG, "loadBitmap() " + uriString);
+ }
+ return doLoadBitmap(
+ context, uriString, maxWidth, maxHeight, callback, IMAGE_THREAD_POOL_EXECUTOR);
+ }
+
+ private static boolean doLoadBitmap(
+ Context context,
+ String uriString,
+ int maxWidth,
+ int maxHeight,
+ ImageLoaderCallback callback,
+ Executor executor) {
+ // Check the cache before creating a Task. The cache will be checked again in doLoadBitmap
+ // but checking a cache is much cheaper than creating an new task.
+ ImageCache imageCache = ImageCache.getInstance();
+ ScaledBitmapInfo bitmapInfo = imageCache.get(uriString);
+ if (bitmapInfo != null && !bitmapInfo.needToReload(maxWidth, maxHeight)) {
+ if (callback != null) {
+ callback.onBitmapLoaded(bitmapInfo.bitmap);
+ }
+ return true;
+ }
+ return doLoadBitmap(
+ callback,
+ executor,
+ new LoadBitmapFromUriTask(context, imageCache, uriString, maxWidth, maxHeight));
+ }
+
+ /**
+ * Load a bitmap image with the cache and resize it with given params.
+ *
+ * <p>The LoadBitmapTask will be executed on a non ui thread.
+ *
+ * @return {@code true} if the load is complete and the callback is executed.
+ */
+ @UiThread
+ public static boolean loadBitmap(ImageLoaderCallback callback, LoadBitmapTask loadBitmapTask) {
+ if (DEBUG) {
+ Log.d(TAG, "loadBitmap() " + loadBitmapTask);
+ }
+ return doLoadBitmap(callback, IMAGE_THREAD_POOL_EXECUTOR, loadBitmapTask);
+ }
+
+ /** @return {@code true} if the load is complete and the callback is executed. */
+ @UiThread
+ private static boolean doLoadBitmap(
+ ImageLoaderCallback callback, Executor executor, LoadBitmapTask loadBitmapTask) {
+ ScaledBitmapInfo bitmapInfo = loadBitmapTask.getFromCache();
+ boolean needToReload = loadBitmapTask.isReloadNeeded();
+ if (bitmapInfo != null && !needToReload) {
+ if (callback != null) {
+ callback.onBitmapLoaded(bitmapInfo.bitmap);
+ }
+ return true;
+ }
+ LoadBitmapTask existingTask = sPendingListMap.get(loadBitmapTask.getKey());
+ if (existingTask != null && !loadBitmapTask.isReloadNeeded(existingTask)) {
+ // The image loading is already scheduled and is large enough.
+ if (callback != null) {
+ existingTask.mCallbacks.add(callback);
+ }
+ } else {
+ if (callback != null) {
+ loadBitmapTask.mCallbacks.add(callback);
+ }
+ sPendingListMap.put(loadBitmapTask.getKey(), loadBitmapTask);
+ try {
+ loadBitmapTask.executeOnExecutor(executor);
+ } catch (RejectedExecutionException e) {
+ Log.e(TAG, "Failed to create new image loader", e);
+ sPendingListMap.remove(loadBitmapTask.getKey());
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Loads and caches a a possibly scaled down version of a bitmap.
+ *
+ * <p>Implement {@link #doGetBitmapInBackground} to do the actual loading.
+ */
+ public abstract static class LoadBitmapTask extends AsyncTask<Void, Void, ScaledBitmapInfo> {
+ protected final Context mAppContext;
+ protected final int mMaxWidth;
+ protected final int mMaxHeight;
+ private final Set<ImageLoaderCallback> mCallbacks = new ArraySet<>();
+ private final ImageCache mImageCache;
+ private final String mKey;
+
+ /**
+ * Returns true if a reload is needed compared to current results in the cache or false if
+ * there is not match in the cache.
+ */
+ private boolean isReloadNeeded() {
+ ScaledBitmapInfo bitmapInfo = getFromCache();
+ boolean needToReload =
+ bitmapInfo != null && bitmapInfo.needToReload(mMaxWidth, mMaxHeight);
+ if (DEBUG) {
+ if (needToReload) {
+ Log.d(
+ TAG,
+ "Bitmap needs to be reloaded. {"
+ + "originalWidth="
+ + bitmapInfo.bitmap.getWidth()
+ + ", originalHeight="
+ + bitmapInfo.bitmap.getHeight()
+ + ", reqWidth="
+ + mMaxWidth
+ + ", reqHeight="
+ + mMaxHeight
+ + "}");
+ }
+ }
+ return needToReload;
+ }
+
+ /** Checks if a reload would be needed if the results of other was available. */
+ private boolean isReloadNeeded(LoadBitmapTask other) {
+ return (other.mMaxHeight != Integer.MAX_VALUE && mMaxHeight >= other.mMaxHeight * 2)
+ || (other.mMaxWidth != Integer.MAX_VALUE && mMaxWidth >= other.mMaxWidth * 2);
+ }
+
+ @Nullable
+ public final ScaledBitmapInfo getFromCache() {
+ return mImageCache.get(mKey);
+ }
+
+ public LoadBitmapTask(
+ Context context, ImageCache imageCache, String key, int maxHeight, int maxWidth) {
+ if (maxWidth == 0 || maxHeight == 0) {
+ throw new IllegalArgumentException(
+ "Image size should not be 0. {width="
+ + maxWidth
+ + ", height="
+ + maxHeight
+ + "}");
+ }
+ mAppContext = context.getApplicationContext();
+ mKey = key;
+ mImageCache = imageCache;
+ mMaxHeight = maxHeight;
+ mMaxWidth = maxWidth;
+ }
+
+ /** Loads the bitmap returning a possibly scaled down version. */
+ @Nullable
+ @WorkerThread
+ public abstract ScaledBitmapInfo doGetBitmapInBackground();
+
+ @Override
+ @Nullable
+ public final ScaledBitmapInfo doInBackground(Void... params) {
+ ScaledBitmapInfo bitmapInfo = getFromCache();
+ if (bitmapInfo != null && !isReloadNeeded()) {
+ return bitmapInfo;
+ }
+ bitmapInfo = doGetBitmapInBackground();
+ if (bitmapInfo != null) {
+ mImageCache.putIfNeeded(bitmapInfo);
+ }
+ return bitmapInfo;
+ }
+
+ @Override
+ public final void onPostExecute(ScaledBitmapInfo scaledBitmapInfo) {
+ if (DEBUG) Log.d(ImageLoader.TAG, "Bitmap is loaded " + mKey);
+
+ for (ImageLoader.ImageLoaderCallback callback : mCallbacks) {
+ callback.onBitmapLoaded(scaledBitmapInfo == null ? null : scaledBitmapInfo.bitmap);
+ }
+ ImageLoader.sPendingListMap.remove(mKey);
+ }
+
+ public final String getKey() {
+ return mKey;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + "("
+ + mKey
+ + " "
+ + mMaxWidth
+ + "x"
+ + mMaxHeight
+ + ")";
+ }
+ }
+
+ private static final class LoadBitmapFromUriTask extends LoadBitmapTask {
+ private LoadBitmapFromUriTask(
+ Context context,
+ ImageCache imageCache,
+ String uriString,
+ int maxWidth,
+ int maxHeight) {
+ super(context, imageCache, uriString, maxHeight, maxWidth);
+ }
+
+ @Override
+ @Nullable
+ public final ScaledBitmapInfo doGetBitmapInBackground() {
+ return BitmapUtils.decodeSampledBitmapFromUriString(
+ mAppContext, getKey(), mMaxWidth, mMaxHeight);
+ }
+ }
+
+ /** Loads and caches the logo for a given {@link TvInputInfo} */
+ public static final class LoadTvInputLogoTask extends LoadBitmapTask {
+ private final TvInputInfo mInfo;
+
+ public LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info) {
+ super(
+ context,
+ cache,
+ getTvInputLogoKey(info.getId()),
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size),
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size));
+ mInfo = info;
+ }
+
+ @Nullable
+ @Override
+ public ScaledBitmapInfo doGetBitmapInBackground() {
+ Drawable drawable = mInfo.loadIcon(mAppContext);
+ if (!(drawable instanceof BitmapDrawable)) {
+ return null;
+ }
+ Bitmap original = ((BitmapDrawable) drawable).getBitmap();
+ if (original == null) {
+ return null;
+ }
+ return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight);
+ }
+
+ /** Returns key of TV input logo. */
+ public static String getTvInputLogoKey(String inputId) {
+ return inputId + "-logo";
+ }
+ }
+
+ private static synchronized Handler getMainHandler() {
+ if (sMainHandler == null) {
+ sMainHandler = new Handler(Looper.getMainLooper());
+ }
+ return sMainHandler;
+ }
+
+ private ImageLoader() {}
+}