Add ImageViewBinder#detach/reattach and extra logs

See the ImageViewBinder javadoc.

Test: manual
Change-Id: I65dee21d0125990c2550b2cfcda7d6b79c86d041
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
index 9b13438..ee50b72 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
@@ -27,7 +27,15 @@
 import com.android.car.apps.common.R;
 
 /**
- * Binds images to an image view.
+ * Binds images to an image view.<p/>
+ * While making a new image request (including passing a null {@link ImageBinder.ImageRef} in
+ * {@link #setImage}) will cancel the current image request (if any), RecyclerView doesn't
+ * always reuse all its views, causing multiple requests to not be canceled. On a slow network,
+ * those requests then take time to execute and can make it look like the application has
+ * stopped loading images if the user keeps browsing. To prevent that, override:
+ * {@link RecyclerView.Adapter#onViewDetachedFromWindow} and call {@link #maybeCancelLoading}
+ * {@link RecyclerView.Adapter#onViewAttachedToWindow} and call {@link #maybeRestartLoading}.
+ *
  * @param <T> see {@link ImageRef}.
  */
 public class ImageViewBinder<T extends ImageBinder.ImageRef> extends ImageBinder<T> {
@@ -36,6 +44,9 @@
     private final ImageView mImageView;
     private final boolean mFlagBitmaps;
 
+    private T mSavedRef;
+    private boolean mCancelled;
+
     /** See {@link ImageViewBinder} and {@link ImageBinder}. */
     public ImageViewBinder(Size maxImageSize, @Nullable ImageView imageView) {
         this(PlaceholderType.FOREGROUND, maxImageSize, imageView, false);
@@ -71,13 +82,39 @@
         }
     }
 
+    /**
+     * Loads a new {@link ImageRef}. The previous request (if any) will be canceled.
+     */
     @Override
     public void setImage(Context context, @Nullable T newRef) {
+        mSavedRef = newRef;
+        mCancelled = false;
         if (mImageView != null) {
             super.setImage(context, newRef);
         }
     }
 
+    /**
+     * Restarts the image loading request if {@link #setImage} was called with a valid reference
+     * that could not be loaded before {@link #maybeCancelLoading} was called.
+     */
+    public void maybeRestartLoading(Context context) {
+        if (mCancelled) {
+            setImage(context, mSavedRef);
+        }
+    }
+
+    /**
+     * Cancels the current loading request (if any) so it doesn't take cycles when the imageView
+     * doesn't need the image (like when the view was moved off screen).
+     */
+    public void maybeCancelLoading(Context context) {
+        mCancelled = true;
+        if (mImageView != null) {
+            super.setImage(context, null); // Call super to keep mSavedRef.
+        }
+    }
+
     @Override
     protected void prepareForNewBinding(Context context) {
         mImageView.setImageBitmap(null);
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java b/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
index dc10caf..e8b245e 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
@@ -62,6 +62,7 @@
 
     private static final String TAG = "LocalImageFetcher";
     private static final boolean L_WARN = Log.isLoggable(TAG, Log.WARN);
+    private static final boolean L_DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int KB = 1024;
     private static final int MB = KB * KB;
@@ -150,6 +151,9 @@
                 task = new ImageLoadingTask(context, key, mFlagRemoteImages);
                 mTasks.put(key, task);
                 task.executeOnExecutor(getThreadPool(packageName));
+                if (L_DEBUG) {
+                    Log.d(TAG, "Added task " + key.mImageUri);
+                }
             } else {
                 Log.e(TAG, "No package for " + key.mImageUri);
             }
@@ -168,8 +172,10 @@
                 ImageLoadingTask task = mTasks.remove(key);
                 if (task != null) {
                     task.cancel(true);
-                }
-                if (L_WARN) {
+                    if (L_DEBUG) {
+                        Log.d(TAG, "Canceled task " + key.mImageUri);
+                    }
+                } else if (L_WARN) {
                     Log.w(TAG, "cancelRequest missing task for: " + key);
                 }
             }
@@ -325,6 +331,10 @@
         @UiThread
         @Override
         protected void onPostExecute(Drawable drawable) {
+            if (L_DEBUG) {
+                Log.d(TAG, "onPostExecute canceled:  " + isCancelled() + " drawable: " + drawable
+                        + " " + mImageKey.mImageUri);
+            }
             if (!isCancelled()) {
                 if (sInstance != null) {
                     sInstance.fulfilRequests(this, drawable);