Merge "Merging multiple implementations of CancellableTask" into main
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 4e6b23f..e6febff 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.Flags.enableOverviewIconMenu;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -41,12 +42,12 @@
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -109,21 +110,17 @@
             callback.accept(task);
             return null;
         }
-        CancellableTask<TaskCacheEntry> request = new CancellableTask<TaskCacheEntry>() {
-            @Override
-            public TaskCacheEntry getResultOnBg() {
-                return getCacheEntry(task);
-            }
-
-            @Override
-            public void handleResult(TaskCacheEntry result) {
-                task.icon = result.icon;
-                task.titleDescription = result.contentDescription;
-                task.title = result.title;
-                callback.accept(task);
-                dispatchIconUpdate(task.key.id);
-            }
-        };
+        CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
+                () -> getCacheEntry(task),
+                MAIN_EXECUTOR,
+                result -> {
+                    task.icon = result.icon;
+                    task.titleDescription = result.contentDescription;
+                    task.title = result.title;
+                    callback.accept(task);
+                    dispatchIconUpdate(task.key.id);
+                }
+        );
         mBgExecutor.execute(request);
         return request;
     }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 9e21595..b7cbb47 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -23,8 +24,8 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
 import com.android.quickstep.util.TaskKeyCache;
 import com.android.quickstep.util.TaskKeyLruCache;
@@ -195,33 +196,29 @@
             return null;
         }
 
-        CancellableTask<ThumbnailData> request = new CancellableTask<>() {
-            @Override
-            public ThumbnailData getResultOnBg() {
-                ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance().getTaskThumbnail(
-                        key.id, lowResolution);
-                if (thumbnailData.thumbnail != null) {
-                    return thumbnailData;
-                }
-                return ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
-            }
-
-            @Override
-            public void handleResult(ThumbnailData result) {
-                // Avoid an async timing issue that a low res entry replaces an existing high res
-                // entry in high res enabled state, so we check before putting it to cache
-                if (enableGridOnlyOverview() && result.reducedResolution
-                        && getHighResLoadingState().isEnabled()) {
-                    ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
-                    if (cachedThumbnail != null && cachedThumbnail.thumbnail != null
-                            && !cachedThumbnail.reducedResolution) {
-                        return;
+        CancellableTask<ThumbnailData> request = new CancellableTask<>(
+                () -> {
+                    ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
+                            .getTaskThumbnail(key.id, lowResolution);
+                    return thumbnailData.thumbnail != null ? thumbnailData
+                            : ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
+                },
+                MAIN_EXECUTOR,
+                result -> {
+                    // Avoid an async timing issue that a low res entry replaces an existing high
+                    // res entry in high res enabled state, so we check before putting it to cache
+                    if (enableGridOnlyOverview() && result.reducedResolution
+                            && getHighResLoadingState().isEnabled()) {
+                        ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
+                        if (newCachedThumbnail != null && newCachedThumbnail.thumbnail != null
+                                && !newCachedThumbnail.reducedResolution) {
+                            return;
+                        }
                     }
+                    mCache.put(key, result);
+                    callback.accept(result);
                 }
-                mCache.put(key, result);
-                callback.accept(result);
-            }
-        };
+        );
         mBgExecutor.execute(request);
         return request;
     }
diff --git a/quickstep/src/com/android/quickstep/util/CancellableTask.java b/quickstep/src/com/android/quickstep/util/CancellableTask.java
deleted file mode 100644
index a6e2e81..0000000
--- a/quickstep/src/com/android/quickstep/util/CancellableTask.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2020 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.quickstep.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-/**
- * Utility class to executore a task on background and post the result on UI thread
- */
-public abstract class CancellableTask<T> implements Runnable {
-
-    private boolean mCancelled = false;
-
-    @Override
-    public final void run() {
-        if (mCancelled) {
-            return;
-        }
-        T result = getResultOnBg();
-        if (mCancelled) {
-            return;
-        }
-        MAIN_EXECUTOR.execute(() -> {
-            if (mCancelled) {
-                return;
-            }
-            handleResult(result);
-        });
-    }
-
-    /**
-     * Called on the worker thread to process the request. The return object is passed to
-     * {@link #handleResult(Object)}
-     */
-    @WorkerThread
-    public abstract T getResultOnBg();
-
-    /**
-     * Called on the UI thread to handle the final result.
-     * @param result
-     */
-    @UiThread
-    public abstract void handleResult(T result);
-
-    /**
-     * Cancels the request. If it is called before {@link #handleResult(Object)}, that method
-     * will not be called
-     */
-    public void cancel() {
-        mCancelled = true;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 6b00473..4c7d5c4 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -44,10 +44,10 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.RunnableList;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskThumbnailCache;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -63,7 +63,6 @@
 import java.util.List;
 import java.util.function.Consumer;
 
-
 /**
  * TaskView that contains all tasks that are part of the desktop.
  */
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index ad4b292..10ef47c 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -24,6 +24,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
@@ -31,7 +32,6 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskThumbnailCache;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.systemui.shared.recents.model.Task;
@@ -45,7 +45,6 @@
 import java.util.HashMap;
 import java.util.function.Consumer;
 
-
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
  *
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index e909418..99c42a7 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -147,7 +147,6 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
@@ -156,6 +155,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -590,8 +590,7 @@
                 return;
             }
             Task.TaskKey taskKey = taskView.getTask().key;
-            UI_HELPER_EXECUTOR.execute(new HandlerRunnable<>(
-                    UI_HELPER_EXECUTOR.getHandler(),
+            UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
                     () -> PackageManagerWrapper.getInstance()
                             .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
                     MAIN_EXECUTOR,
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index ba39844..085c502 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -54,7 +54,6 @@
 import android.graphics.Canvas;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.util.AttributeSet;
@@ -89,6 +88,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -107,7 +107,6 @@
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.BorderAnimator;
-import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskCornerRadius;
@@ -135,8 +134,6 @@
     private static final String TAG = TaskView.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    private static final RectF EMPTY_RECT_F = new RectF();
-
     public static final int FLAG_UPDATE_ICON = 1;
     public static final int FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON << 1;
     public static final int FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL << 1;
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index c4ef4ae..259ddbd 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -69,13 +69,13 @@
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.icons.PlaceHolderIconDrawable;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.search.StringMatcherUtility;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.SafeCloseable;
@@ -190,7 +190,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mDisableRelayout = false;
 
-    private HandlerRunnable mIconLoadRequest;
+    private CancellableTask mIconLoadRequest;
 
     private boolean mEnableIconUpdateAnimation = false;
 
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index d8fa90a..ee66a60 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -54,7 +54,6 @@
 import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
 import com.android.launcher3.icons.cache.BaseIconCache;
 import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -63,6 +62,7 @@
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
@@ -100,7 +100,7 @@
     private final UserCache mUserManager;
     private final InstantAppResolver mInstantAppResolver;
     private final IconProvider mIconProvider;
-    private final HandlerRunnable mCancelledRunnable;
+    private final CancellableTask mCancelledTask;
 
     private final SparseArray<BitmapInfo> mWidgetCategoryBitmapInfos;
 
@@ -119,9 +119,8 @@
         mIconProvider = iconProvider;
         mWidgetCategoryBitmapInfos = new SparseArray<>();
 
-        mCancelledRunnable = new HandlerRunnable(
-                mWorkerHandler, () -> null, MAIN_EXECUTOR, c -> { });
-        mCancelledRunnable.cancel();
+        mCancelledTask = new CancellableTask(() -> null, MAIN_EXECUTOR, c -> { });
+        mCancelledTask.cancel();
     }
 
     @Override
@@ -174,7 +173,7 @@
      *
      * @return a request ID that can be used to cancel the request.
      */
-    public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller,
+    public CancellableTask updateIconInBackground(final ItemInfoUpdateReceiver caller,
             final ItemInfoWithIcon info) {
         Preconditions.assertUIThread();
         Supplier<ItemInfoWithIcon> task;
@@ -191,7 +190,7 @@
         } else {
             Log.i(TAG, "Icon update not supported for "
                     + info == null ? "null" : info.getClass().getName());
-            return mCancelledRunnable;
+            return mCancelledTask;
         }
 
         if (mPendingIconRequestCount <= 0) {
@@ -199,7 +198,7 @@
         }
         mPendingIconRequestCount++;
 
-        HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler,
+        CancellableTask<ItemInfoWithIcon> request = new CancellableTask<>(
                 task, MAIN_EXECUTOR, caller::reapplyItemInfo, this::onIconRequestEnd);
         Utilities.postAsyncCallback(mWorkerHandler, request);
         return request;
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 71957e1..43027da 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -23,7 +23,7 @@
 import com.android.launcher3.BubbleTextView
 import com.android.launcher3.allapps.BaseAllAppsAdapter
 import com.android.launcher3.config.FeatureFlags
-import com.android.launcher3.util.ExecutorRunnable
+import com.android.launcher3.util.CancellableTask
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
 import com.android.launcher3.views.ActivityContext
@@ -39,7 +39,7 @@
 class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
 
     var hasWorkProfile = false
-    var executorRunnable: ExecutorRunnable<List<ViewHolder>>? = null
+    private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
 
     /**
      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
@@ -62,14 +62,14 @@
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
-        executorRunnable?.cancel(/* interrupt= */ false)
-        executorRunnable =
-            ExecutorRunnable.createAndExecute(
-                VIEW_PREINFLATION_EXECUTOR,
+        mCancellableTask?.cancel()
+        var task: CancellableTask<List<ViewHolder>>? = null
+        task =
+            CancellableTask(
                 {
                     val list: ArrayList<ViewHolder> = ArrayList()
                     for (i in 0 until preInflateCount) {
-                        if (Thread.interrupted()) {
+                        if (task?.canceled == true) {
                             break
                         }
                         list.add(
@@ -85,6 +85,8 @@
                     }
                 }
             )
+        mCancellableTask = task
+        VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
     }
 
     /**
@@ -93,7 +95,7 @@
      */
     override fun clear() {
         super.clear()
-        executorRunnable?.cancel(/* interrupt= */ true)
+        mCancellableTask?.cancel()
     }
 
     /**
diff --git a/src/com/android/launcher3/util/CancellableTask.kt b/src/com/android/launcher3/util/CancellableTask.kt
new file mode 100644
index 0000000..49ef020
--- /dev/null
+++ b/src/com/android/launcher3/util/CancellableTask.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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.launcher3.util
+
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import java.util.function.Supplier
+
+/** A [Runnable] that can be posted to a [Executor] that can be cancelled. */
+class CancellableTask<T>
+@JvmOverloads
+constructor(
+    private val task: Supplier<T>,
+    // Executor where consumer needs to be executed on. Typically UI executor.
+    private val callbackExecutor: Executor,
+    // Consumer that needs to be accepted upon completion of the task. Typically work that needs to
+    // be done in UI thread after task completes.
+    private val callback: Consumer<T>,
+    // Callback to be executed on callbackExecutor at the end irrespective of the task being
+    // completed or cancelled
+    private val endRunnable: Runnable = Runnable {}
+) : Runnable {
+
+    // flag to cancel the callback
+    var canceled = false
+        private set
+
+    private var ended = false
+
+    override fun run() {
+        if (canceled) return
+        val value = task.get()
+        callbackExecutor.execute {
+            if (!canceled) {
+                callback.accept(value)
+            }
+            onEnd()
+        }
+    }
+
+    /**
+     * Cancel the [CancellableTask] if not scheduled. If [CancellableTask] has started execution at
+     * this time, we will try to cancel the callback if not executed yet.
+     */
+    fun cancel() {
+        canceled = true
+        callbackExecutor.execute(this::onEnd)
+    }
+
+    private fun onEnd() {
+        if (!ended) {
+            ended = true
+            endRunnable.run()
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ExecutorRunnable.kt b/src/com/android/launcher3/util/ExecutorRunnable.kt
deleted file mode 100644
index 49cf592..0000000
--- a/src/com/android/launcher3/util/ExecutorRunnable.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 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.launcher3.util
-
-import java.util.concurrent.Executor
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Future
-import java.util.function.Consumer
-import java.util.function.Supplier
-
-/** A [Runnable] that can be posted to a [Executor] that can be cancelled. */
-class ExecutorRunnable<T>
-private constructor(
-    private val task: Supplier<T>,
-    // Executor where consumer needs to be executed on. Typically UI executor.
-    private val callbackExecutor: Executor,
-    // Consumer that needs to be accepted upon completion of the task. Typically work that needs to
-    // be done in UI thread after task completes.
-    private val callback: Consumer<T>
-) : Runnable {
-
-    // future of this runnable that will used for cancellation.
-    lateinit var future: Future<*>
-
-    // flag to cancel the callback
-    var canceled = false
-
-    override fun run() {
-        val value: T = task.get()
-        callbackExecutor.execute {
-            if (!canceled) {
-                callback.accept(value)
-            }
-        }
-    }
-
-    /**
-     * Cancel the [ExecutorRunnable] if not scheduled. If [ExecutorRunnable] has started execution
-     * at this time, we will try to cancel the callback if not executed yet.
-     */
-    fun cancel(interrupt: Boolean) {
-        future.cancel(interrupt)
-        canceled = true
-    }
-
-    companion object {
-        /**
-         * Create [ExecutorRunnable] and execute it on task [Executor]. It will also save the
-         * [Future] into this [ExecutorRunnable] to be used for cancellation.
-         */
-        fun <T> createAndExecute(
-            // Executor where task will be executed, typically an Executor running on background
-            // thread.
-            taskExecutor: ExecutorService,
-            task: Supplier<T>,
-            callbackExecutor: Executor,
-            callback: Consumer<T>
-        ): ExecutorRunnable<T> {
-            val executorRunnable = ExecutorRunnable(task, callbackExecutor, callback)
-            executorRunnable.future = taskExecutor.submit(executorRunnable)
-            return executorRunnable
-        }
-    }
-}
diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
index 99485be..aab78bd 100644
--- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
+++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
@@ -41,9 +41,9 @@
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.ShadowGenerator;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.util.WidgetSizes;
@@ -74,12 +74,12 @@
      * @return a request id which can be used to cancel the request.
      */
     @NonNull
-    public HandlerRunnable loadPreview(
+    public CancellableTask loadPreview(
             @NonNull WidgetItem item,
             @NonNull Size previewSize,
             @NonNull Consumer<Bitmap> callback) {
         Handler handler = Executors.UI_HELPER_EXECUTOR.getHandler();
-        HandlerRunnable<Bitmap> request = new HandlerRunnable<>(handler,
+        CancellableTask<Bitmap> request = new CancellableTask<>(
                 () -> generatePreview(item, previewSize.getWidth(), previewSize.getHeight()),
                 MAIN_EXECUTOR,
                 callback);
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 94c630a..7345511 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -51,8 +51,8 @@
 import com.android.launcher3.R;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.RoundDrawableWrapper;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.function.Consumer;
@@ -90,7 +90,7 @@
 
     private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
 
-    protected HandlerRunnable mActiveRequest;
+    protected CancellableTask mActiveRequest;
     private boolean mAnimatePreview = true;
 
     protected final ActivityContext mActivity;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index 3383299..d373a3b 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -35,9 +35,9 @@
 import com.android.launcher3.R;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.icons.PlaceHolderIconDrawable;
-import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 
@@ -57,7 +57,7 @@
      * widget apps aren't collapsable.
     */
     private final boolean mIsCollapsable;
-    @Nullable private HandlerRunnable mIconLoadRequest;
+    @Nullable private CancellableTask mIconLoadRequest;
     @Nullable private Drawable mIconDrawable;
     @Nullable private WidgetsListDrawableState mListDrawableState;
     private ImageView mAppIcon;
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ExecutorRunnableTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/CancellableTaskTest.kt
similarity index 92%
rename from tests/multivalentTests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/CancellableTaskTest.kt
index 6634577..eb58096 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/CancellableTaskTest.kt
@@ -29,12 +29,12 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-/** Unit test for [ExecutorRunnable] */
+/** Unit test for [CancellableTask] */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ExecutorRunnableTest {
+class CancellableTaskTest {
 
-    private lateinit var underTest: ExecutorRunnable<Int>
+    private lateinit var underTest: CancellableTask<Int>
 
     private val lock = ReentrantLock()
     private var result: Int = -1
@@ -51,8 +51,7 @@
 
     private fun submitJob() {
         underTest =
-            ExecutorRunnable.createAndExecute(
-                Executors.UI_HELPER_EXECUTOR,
+            CancellableTask(
                 {
                     isTaskExecuted = true
                     1
@@ -63,6 +62,7 @@
                     result = it + 1
                 }
             )
+        Executors.UI_HELPER_EXECUTOR.execute(underTest)
     }
 
     @Test
@@ -82,7 +82,7 @@
         Executors.UI_HELPER_EXECUTOR.submit { lock.lock() }
         submitJob()
 
-        underTest.cancel(false)
+        underTest.cancel()
 
         lock.unlock() // unblock task on UI_HELPER_EXECUTOR
         awaitAllExecutorCompleted()
@@ -101,7 +101,7 @@
         awaitExecutorCompleted(Executors.UI_HELPER_EXECUTOR)
         assertTrue("task should be executed.", isTaskExecuted)
 
-        underTest.cancel(false)
+        underTest.cancel()
 
         lock.unlock() // unblock callback on VIEW_PREINFLATION_EXECUTOR
         awaitExecutorCompleted(Executors.VIEW_PREINFLATION_EXECUTOR)
@@ -113,7 +113,7 @@
     fun run_and_cancelAfterCompletion_executeAll() {
         awaitAllExecutorCompleted()
 
-        underTest.cancel(false)
+        underTest.cancel()
 
         assertTrue("task should be executed", isTaskExecuted)
         assertTrue("callback should be executed", isCallbackExecuted)