Reducing delay in applying widget previews loaded in the background.

- Push the writing of previews to the DB to the worker thread
  to ensure that we can apply the loaded preview as soon as
  possible.
- Reducing additional bitmap allocations from cancelled tasks
  that did not re-add their bitmaps to the unused set
  (happens when the list is scrolling quickly).

Change-Id: Ib7c2cfdfcc668c215b3a74271e1165235fbdf752
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 8459673..bbcdaae 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -24,17 +24,16 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
+import android.os.Handler;
 import android.os.Process;
 import android.util.Log;
 import android.util.LongSparseArray;
-
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.widget.WidgetCell;
-
 import junit.framework.Assert;
 
 import java.util.ArrayList;
@@ -69,6 +68,7 @@
     private final CacheDb mDb;
 
     private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
+    private final Handler mWorkerHandler;
 
     public WidgetPreviewLoader(Context context, IconCache iconCache) {
         mContext = context;
@@ -76,6 +76,7 @@
         mManager = AppWidgetManagerCompat.getInstance(context);
         mUserManager = UserManagerCompat.getInstance(context);
         mDb = new CacheDb(context);
+        mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
     }
 
     /**
@@ -83,8 +84,6 @@
      * called on UI thread
      *
      * @param o either {@link LauncherAppWidgetProviderInfo} or {@link ResolveInfo}
-     * @param immediateResult A bitmap array of size 1. If the result is already cached, it is
-     * set to the final result.
      * @return a request id which can be used to cancel the request.
      */
     public PreviewLoadRequest getPreview(final Object o, int previewWidth, int previewHeight,
@@ -289,7 +288,10 @@
         }
     }
 
-    private Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle) {
+    /**
+     * Reads the preview bitmap from the DB or null if the preview is not in the DB.
+     */
+    private Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
         Cursor cursor = null;
         try {
             cursor = mDb.getReadableDatabase().query(
@@ -302,12 +304,18 @@
                             key.size
                     },
                     null, null, null);
+            // If cancelled, skip getting the blob and decoding it into a bitmap
+            if (loadTask.isCancelled()) {
+                return null;
+            }
             if (cursor.moveToNext()) {
                 byte[] blob = cursor.getBlob(0);
                 BitmapFactory.Options opts = new BitmapFactory.Options();
                 opts.inBitmap = recycle;
                 try {
-                    return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
+                    if (!loadTask.isCancelled()) {
+                        return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
+                    }
                 } catch (Exception e) {
                     return null;
                 }
@@ -545,15 +553,17 @@
                 mTask.cancel(true);
             }
 
-            if (mTask.mBitmap == null) {
-                return;
+            // This only handles the case where the PreviewLoadTask is cancelled after the task has
+            // successfully completed (including having written to disk when necessary).  In the
+            // other cases where it is cancelled while the task is running, it will be cleaned up
+            // in the tasks's onCancelled() call, and if cancelled while the task is writing to
+            // disk, it will be cancelled in the task's onPostExecute() call.
+            if (mTask.mBitmapToRecycle != null) {
+                synchronized (mUnusedBitmaps) {
+                    mUnusedBitmaps.add(mTask.mBitmapToRecycle);
+                }
+                mTask.mBitmapToRecycle = null;
             }
-
-            // The preview is no longer bound to any view, move it to {@link WeakReference} list.
-            synchronized(mUnusedBitmaps) {
-                mUnusedBitmaps.add(mTask.mBitmap);
-            }
-            mTask.mBitmap = null;
         }
     }
 
@@ -564,7 +574,8 @@
         private final int mPreviewHeight;
         private final int mPreviewWidth;
         private final WidgetCell mCaller;
-        private Bitmap mBitmap;
+        private long[] mVersions;
+        private Bitmap mBitmapToRecycle;
 
         PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth,
                 int previewHeight, WidgetCell caller) {
@@ -584,6 +595,10 @@
             Bitmap unusedBitmap = null;
 
             synchronized (mUnusedBitmaps) {
+                // If already cancelled before this gets to run in the background, then return early
+                if (isCancelled()) {
+                    return null;
+                }
                 // Check if we can use a bitmap
                 for (Bitmap candidate : mUnusedBitmaps) {
                     if (candidate != null && candidate.isMutable() &&
@@ -600,31 +615,64 @@
                     mUnusedBitmaps.remove(unusedBitmap);
                 }
             }
+            // If cancelled now, don't bother reading the preview from the DB
             if (isCancelled()) {
-                return null;
+                return unusedBitmap;
             }
-            Bitmap preview = readFromDb(mKey, unusedBitmap);
+            Bitmap preview = readFromDb(mKey, unusedBitmap, this);
+            // Only consider generating the preview if we have not cancelled the task already
             if (!isCancelled() && preview == null) {
                 // Fetch the version info before we generate the preview, so that, in-case the
                 // app was updated while we are generating the preview, we use the old version info,
                 // which would gets re-written next time.
-                long[] versions = getPackageVersion(mKey.componentName.getPackageName());
+                mVersions = getPackageVersion(mKey.componentName.getPackageName());
 
                 // it's not in the db... we need to generate it
                 preview = generatePreview(mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight);
-
-                if (!isCancelled()) {
-                    writeToDb(mKey, versions, preview);
-                }
             }
-
             return preview;
         }
 
         @Override
-        protected void onPostExecute(Bitmap result) {
-            mBitmap = result;
-            mCaller.applyPreview(result);
+        protected void onPostExecute(final Bitmap preview) {
+            mCaller.applyPreview(preview);
+
+            // Write the generated preview to the DB in the worker thread
+            if (mVersions != null) {
+                mWorkerHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!isCancelled()) {
+                            // If we are still using this preview, then write it to the DB and then
+                            // let the normal clear mechanism recycle the bitmap
+                            writeToDb(mKey, mVersions, preview);
+                            mBitmapToRecycle = preview;
+                        } else {
+                            // If we've already cancelled, then skip writing the bitmap to the DB
+                            // and manually add the bitmap back to the recycled set
+                            synchronized (mUnusedBitmaps) {
+                                mUnusedBitmaps.add(preview);
+                            }
+                        }
+                    }
+                });
+            } else {
+                // If we don't need to write to disk, then ensure the preview gets recycled by
+                // the normal clear mechanism
+                mBitmapToRecycle = preview;
+            }
+        }
+
+        @Override
+        protected void onCancelled(Bitmap preview) {
+            // If we've cancelled while the task is running, then can return the bitmap to the
+            // recycled set immediately. Otherwise, it will be recycled after the preview is written
+            // to disk.
+            if (preview != null) {
+                synchronized (mUnusedBitmaps) {
+                    mUnusedBitmaps.add(preview);
+                }
+            }
         }
     }