Merge "Adjusting weight of taskbar all apps icon or action key" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 25db4d7..147cac6 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -16,6 +16,13 @@
 }
 
 flag {
+    name: "enable_twoline_toggle"
+    namespace: "launcher"
+    description: "Enables visibility in home settings to see the toggle to turn on/off two lines in all apps."
+    bug: "316027081"
+}
+
+flag {
     name: "enable_grid_only_overview"
     namespace: "launcher"
     description: "Enable a grid-only overview without a focused task."
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index caf8a0b..e1443d0 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderRow;
@@ -137,7 +138,9 @@
         int totalHeight = iconHeight + iconPadding + textHeight + mVerticalPadding * 2;
         // Prediction row height will be 4dp bigger than the regular apps in A-Z list when two line
         // is not enabled. Otherwise, the extra height will increase by just the textHeight.
-        int extraHeight = FeatureFlags.enableTwolineAllapps() ? textHeight : mTopRowExtraHeight;
+        int extraHeight = (FeatureFlags.enableTwolineAllapps() && (!Flags.enableTwolineToggle()
+                || (Flags.enableTwolineToggle() && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(
+                        getContext())))) ? textHeight : mTopRowExtraHeight;
         totalHeight += extraHeight;
         return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b69f657..9006df8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -332,7 +332,7 @@
 
             // Update icon size
             deviceProfile.iconSizePx = deviceProfile.taskbarIconSize;
-            deviceProfile.updateIconSize(1f, getResources());
+            deviceProfile.updateIconSize(1f, this);
         };
         mDeviceProfile = originDeviceProfile.toBuilder(this)
                 .withDimensionsOverride(overrideProvider).build();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 8a26054..af1bd2a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -260,7 +260,8 @@
                                     taskAttributes.getThumbnailView(),
                                     taskAttributes.getThumbnailView().getThumbnail(),
                                     null /* intent */,
-                                    null /* user */);
+                                    null /* user */,
+                                    info);
                             return;
                         }
                     }
@@ -273,7 +274,8 @@
                             startingView,
                             null /* thumbnail */,
                             intent,
-                            info.user);
+                            info.user,
+                            info);
                 }
         );
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 9329e16..dbaeafb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -22,6 +22,7 @@
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -111,7 +112,7 @@
         boolean exitingOverview = !FeatureFlags.enableSplitContextually() && !toState.overviewUi;
         if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
             setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
-                    .createPlaceholderDismissAnim(mLauncher));
+                    .createPlaceholderDismissAnim(mLauncher, LAUNCHER_SPLIT_SELECTION_EXIT_HOME));
             setter.setViewAlpha(
                     mRecentsView.getSplitInstructionsView(),
                     0,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index f3f36c5..722676a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -39,6 +39,8 @@
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
@@ -596,7 +598,7 @@
         list.add(getDragController());
         Consumer<AnimatorSet> splitAnimator = animatorSet ->
                 animatorSet.play(mSplitSelectStateController.getSplitAnimationController()
-                        .createPlaceholderDismissAnim(this));
+                        .createPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_HOME));
         switch (mode) {
             case NO_BUTTON:
                 list.add(new NoButtonQuickSwitchTouchController(this));
@@ -767,8 +769,10 @@
             // If Launcher pauses before both split apps are selected, exit split screen.
             if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
                     !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
+                mSplitSelectStateController
+                        .logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
                 mSplitSelectStateController.getSplitAnimationController()
-                        .playPlaceholderDismissAnim(this);
+                        .playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
             }
         }
     }
@@ -1042,17 +1046,17 @@
     }
 
     @Override
-    protected void handleSplitAnimationGoingToHome() {
-        super.handleSplitAnimationGoingToHome();
+    protected void handleSplitAnimationGoingToHome(StatsLogManager.EventEnum splitDismissReason) {
+        super.handleSplitAnimationGoingToHome(splitDismissReason);
         mSplitSelectStateController.getSplitAnimationController()
-                .playPlaceholderDismissAnim(this);
+                .playPlaceholderDismissAnim(this, splitDismissReason);
     }
 
     @Override
-    public void dismissSplitSelection() {
-        super.dismissSplitSelection();
+    public void dismissSplitSelection(StatsLogManager.LauncherEvent splitDismissEvent) {
+        super.dismissSplitSelection(splitDismissEvent);
         mSplitSelectStateController.getSplitAnimationController()
-                .playPlaceholderDismissAnim(this);
+                .playPlaceholderDismissAnim(this, splitDismissEvent);
     }
 
     public <T extends OverviewActionsView> T getActionsView() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 5db5d17..b5914a1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -186,7 +186,7 @@
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 InteractionJankMonitorWrapper.begin(
-                        mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS);
+                        mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS, /*tag=*/ "swipe");
                 InteractionJankMonitorWrapper.begin(
                         mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE);
                 break;
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/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 8f9395f..b846323 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -174,10 +174,10 @@
 
                     @Nullable Task foundTask2 = foundTasks[1];
                     if (foundTask2 != null) {
-                        mSplitSelectStateController.setSecondTask(foundTask2);
+                        mSplitSelectStateController.setSecondTask(foundTask2, app2);
                     } else {
                         mSplitSelectStateController.setSecondTask(
-                                app2.intent, app2.user);
+                                app2.intent, app2.user, app2);
                     }
 
                     mSplitSelectStateController.setLaunchingIconView(appPairIcon);
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/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 0ee27be..1eb71fc 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -40,22 +40,26 @@
 import androidx.annotation.VisibleForTesting
 import com.android.app.animation.Interpolators
 import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Launcher
 import com.android.launcher3.QuickstepTransitionManager
 import com.android.launcher3.Utilities
 import com.android.launcher3.anim.PendingAnimation
 import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.logging.StatsLogManager.EventEnum
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.statemanager.StatefulActivity
 import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
 import com.android.launcher3.views.BaseDragLayer
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.views.FloatingAppPairView
 import com.android.quickstep.views.FloatingTaskView
 import com.android.quickstep.views.GroupedTaskView
+import com.android.quickstep.views.IconAppChipView
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.SplitInstructionsView
 import com.android.quickstep.views.TaskThumbnailView
@@ -155,16 +159,36 @@
         val iconView: View = taskIdAttributeContainer.iconView.asView()
         builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f))
         thumbnail.setShowSplashForSplitSelection(true)
+        // With the new `IconAppChipView`, we always want to keep the chip pinned to the
+        // top left of the task / thumbnail.
+        if (enableOverviewIconMenu()) {
+            builder.add(
+                ObjectAnimator.ofFloat(
+                    (iconView as IconAppChipView).splitTranslationX,
+                    MULTI_PROPERTY_VALUE,
+                    0f
+                )
+            )
+            builder.add(
+                ObjectAnimator.ofFloat(
+                    iconView.splitTranslationY,
+                    MULTI_PROPERTY_VALUE,
+                    0f
+                )
+            )
+        }
         if (deviceProfile.isLeftRightSplit) {
             // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
             val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f
-            val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f
             val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width
             builder.add(ObjectAnimator.ofFloat(thumbnail,
                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX))
-            // icons are anchored from Gravity.END, so need to use negative translation
-            builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
+            if (!enableOverviewIconMenu()) {
+                // icons are anchored from Gravity.END, so need to use negative translation
+                val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f
+                builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
                     -centerIconTranslationX))
+            }
             builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX))
 
             // Reset other dimensions
@@ -183,7 +207,6 @@
             // asymmetry causes problems..
 
             // Icon defaults to center | horizontal, we add additional translation for split
-            val centerIconTranslationX = 0f
             var centerThumbnailTranslationY: Float
 
             // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary
@@ -200,9 +223,10 @@
             builder.add(ObjectAnimator.ofFloat(thumbnail,
                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY))
 
-            // icons are anchored from Gravity.END, so need to use negative translation
-            builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
-                    centerIconTranslationX))
+            if (!enableOverviewIconMenu())  {
+                // icons are anchored from Gravity.END, so need to use negative translation
+                builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f))
+            }
             builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY))
 
             // Reset other dimensions
@@ -213,17 +237,21 @@
     }
 
     /** Does not play any animation if user is not currently in split selection state. */
-    fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>) {
+    fun playPlaceholderDismissAnim(launcher: StatefulActivity<*>, splitDismissEvent: EventEnum) {
         if (!splitSelectStateController.isSplitSelectActive) {
             return
         }
 
-        val anim = createPlaceholderDismissAnim(launcher)
+        val anim = createPlaceholderDismissAnim(launcher, splitDismissEvent)
         anim.start()
     }
 
-    /** Returns [AnimatorSet] which slides initial split placeholder view offscreen. */
-    fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>) : AnimatorSet {
+    /**
+     * Returns [AnimatorSet] which slides initial split placeholder view offscreen and logs an event
+     * for why split is being dismissed
+     */
+    fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>,
+                                     splitDismissEvent: EventEnum) : AnimatorSet {
         val animatorSet = AnimatorSet()
         val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
         val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
@@ -260,6 +288,7 @@
                         splitSelectStateController.splitInstructionsView)
             }
         })
+        splitSelectStateController.logExitReason(splitDismissEvent)
         return animatorSet
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
index c013483..06edb14 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
@@ -87,6 +87,7 @@
     @StagePosition
     private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
     private var itemInfo: ItemInfo? = null
+    private var secondItemInfo: ItemInfo? = null
     private var splitEvent: EventEnum? = null
 
     private var initialTaskId: Int = INVALID_TASK_ID
@@ -144,8 +145,9 @@
      * To be called as soon as user selects the second task (even if animations aren't complete)
      * @param taskId The second task that will be launched.
      */
-    fun setSecondTask(taskId: Int) {
+    fun setSecondTask(taskId: Int, itemInfo: ItemInfo) {
         secondTaskId = taskId
+        secondItemInfo = itemInfo
     }
 
     /**
@@ -153,9 +155,10 @@
      * @param intent The second intent that will be launched.
      * @param user The user of that intent.
      */
-    fun setSecondTask(intent: Intent, user: UserHandle) {
+    fun setSecondTask(intent: Intent, user: UserHandle, itemInfo: ItemInfo) {
         secondIntent = intent
         secondUser = user
+        secondItemInfo = itemInfo
     }
 
     /**
@@ -163,9 +166,10 @@
      * Sets [secondUser] from that of the pendingIntent
      * @param pendingIntent The second PendingIntent that will be launched.
      */
-    fun setSecondTask(pendingIntent: PendingIntent) {
+    fun setSecondTask(pendingIntent: PendingIntent, itemInfo: ItemInfo) {
         secondPendingIntent = pendingIntent
         secondUser = pendingIntent.creatorUserHandle
+        secondItemInfo = itemInfo
     }
 
     /**
@@ -173,8 +177,8 @@
      * an extra intent from their RemoteResponse.
      * See [android.widget.RemoteViews.RemoteResponse.getLaunchOptions].first
      */
-    fun setSecondWidget(pendingIntent: PendingIntent, widgetIntent: Intent?) {
-        setSecondTask(pendingIntent)
+    fun setSecondWidget(pendingIntent: PendingIntent, widgetIntent: Intent?, itemInfo: ItemInfo) {
+        setSecondTask(pendingIntent, itemInfo)
         widgetSecondIntent = widgetIntent
     }
 
@@ -407,6 +411,10 @@
         return itemInfo
     }
 
+    fun getSecondItemInfo(): ItemInfo? {
+        return secondItemInfo
+    }
+
     private fun isSecondTaskIntentSet(): Boolean {
         return secondTaskId != INVALID_TASK_ID || secondIntent != null
                 || secondPendingIntent != null
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 3c90e0c..f06418b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -19,6 +19,10 @@
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_COMPLETE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_INITIATED;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -151,6 +155,11 @@
     private SplitInstructionsView mSplitInstructionsView;
 
     private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>();
+    /**
+     * Tracks metrics from when first app is selected to split launch or cancellation. This also
+     * gets passed over to shell when attempting to invoke split.
+     */
+    private Pair<InstanceId, com.android.launcher3.logging.InstanceId> mSessionInstanceIds;
 
     private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
         @Override
@@ -162,7 +171,8 @@
         public void onBackInvoked() {
             // When exiting from split selection, leave current context to go to
             // homescreen as well
-            getSplitAnimationController().playPlaceholderDismissAnim(mContext);
+            getSplitAnimationController().playPlaceholderDismissAnim(mContext,
+                    LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
             if (mActivityBackCallback != null) {
                 mActivityBackCallback.run();
             }
@@ -203,6 +213,7 @@
             int alreadyRunningTask) {
         mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
                 alreadyRunningTask);
+        createAndLogInstanceIdsForSession();
     }
 
     /**
@@ -213,6 +224,7 @@
             @StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
             StatsLogManager.EventEnum splitEvent) {
         mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
+        createAndLogInstanceIdsForSession();
     }
 
     /**
@@ -330,14 +342,12 @@
      */
     public void launchSplitTasks(@PersistentSnapPosition int snapPosition,
             @Nullable Consumer<Boolean> callback) {
-        Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
-                LogUtils.getShellShareableInstanceId();
-        launchTasks(callback, false /* freezeTaskList */, snapPosition, instanceIds.first);
+        launchTasks(callback, false /* freezeTaskList */, snapPosition, mSessionInstanceIds.first);
 
         mStatsLogManager.logger()
-                .withItemInfo(mSplitSelectDataHolder.getItemInfo())
-                .withInstanceId(instanceIds.second)
-                .log(mSplitSelectDataHolder.getSplitEvent());
+                .withItemInfo(mSplitSelectDataHolder.getSecondItemInfo())
+                .withInstanceId(mSessionInstanceIds.second)
+                .log(LAUNCHER_SPLIT_SELECTED_SECOND_APP);
     }
 
     /**
@@ -364,11 +374,26 @@
     }
 
     /**
+     * Use to log an event when user exists split selection when the second app **IS NOT** selected.
+     * This must be called before playing any exit animations since most animations will call
+     * {@link #resetState()} which removes {@link #mSessionInstanceIds}.
+     */
+    public void logExitReason(StatsLogManager.EventEnum splitExitEvent) {
+        StatsLogManager.StatsLogger logger = mStatsLogManager.logger();
+        if (mSessionInstanceIds != null) {
+            logger.withInstanceId(mSessionInstanceIds.second);
+        } else {
+            Log.w(TAG, "Missing session instanceIds");
+        }
+        logger.log(splitExitEvent);
+    }
+
+    /**
      * To be called as soon as user selects the second task (even if animations aren't complete)
      * @param task The second task that will be launched.
      */
-    public void setSecondTask(Task task) {
-        mSplitSelectDataHolder.setSecondTask(task.key.id);
+    public void setSecondTask(Task task, ItemInfo itemInfo) {
+        mSplitSelectDataHolder.setSecondTask(task.key.id, itemInfo);
     }
 
     /**
@@ -376,20 +401,20 @@
      * @param intent The second intent that will be launched.
      * @param user The user of that intent.
      */
-    public void setSecondTask(Intent intent, UserHandle user) {
-        mSplitSelectDataHolder.setSecondTask(intent, user);
+    public void setSecondTask(Intent intent, UserHandle user, ItemInfo itemInfo) {
+        mSplitSelectDataHolder.setSecondTask(intent, user, itemInfo);
     }
 
     /**
      * To be called as soon as user selects the second app (even if animations aren't complete)
      * @param pendingIntent The second PendingIntent that will be launched.
      */
-    public void setSecondTask(PendingIntent pendingIntent) {
-        mSplitSelectDataHolder.setSecondTask(pendingIntent);
+    public void setSecondTask(PendingIntent pendingIntent, ItemInfo itemInfo) {
+        mSplitSelectDataHolder.setSecondTask(pendingIntent, itemInfo);
     }
 
     public void setSecondWidget(PendingIntent pendingIntent, Intent widgetIntent) {
-        mSplitSelectDataHolder.setSecondWidget(pendingIntent, widgetIntent);
+        mSplitSelectDataHolder.setSecondWidget(pendingIntent, widgetIntent, null /*itemInfo*/);
     }
 
     /**
@@ -578,7 +603,7 @@
         final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
                 ActivityThread.currentActivityThread().getApplicationThread(),
                 "LaunchAppFullscreen");
-        InstanceId instanceId = LogUtils.getShellShareableInstanceId().first;
+        InstanceId instanceId = mSessionInstanceIds.first;
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             switch (launchData.getSplitLaunchType()) {
                 case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
@@ -630,6 +655,26 @@
                 ActivityThread.currentActivityThread().getApplicationThread());
     }
 
+    /**
+     * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from
+     * {@link #mSplitSelectDataHolder}
+     */
+    private void createAndLogInstanceIdsForSession() {
+        if (mSessionInstanceIds != null) {
+            Log.w(TAG, "SessionIds should be null");
+        }
+        // Log separately the start of the session and then the first app selected
+        mSessionInstanceIds = LogUtils.getShellShareableInstanceId();
+        mStatsLogManager.logger()
+                .withInstanceId(mSessionInstanceIds.second)
+                .log(LAUNCHER_SPLIT_SELECTION_INITIATED);
+
+        mStatsLogManager.logger()
+                .withItemInfo(mSplitSelectDataHolder.getItemInfo())
+                .withInstanceId(mSessionInstanceIds.second)
+                .log(mSplitSelectDataHolder.getSplitEvent());
+    }
+
     public @StagePosition int getActiveSplitStagePosition() {
         return mSplitSelectDataHolder.getInitialStagePosition();
     }
@@ -804,6 +849,13 @@
         mFirstFloatingTaskView = null;
         mSplitInstructionsView = null;
         mLaunchingFirstAppFullscreen = false;
+
+        if (mSessionInstanceIds != null) {
+            mStatsLogManager.logger()
+                    .withInstanceId(mSessionInstanceIds.second)
+                    .log(LAUNCHER_SPLIT_SELECTION_COMPLETE);
+        }
+        mSessionInstanceIds = null;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 50a5a83..445a540 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.quickstep.views.FloatingTaskView;
@@ -134,7 +135,7 @@
             return false;
         }
 
-        mController.setSecondTask(intent, user);
+        mController.setSecondTask(intent, user, (ItemInfo) tag);
 
         startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher));
         return 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/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
index 6bb98e1..ba42594 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -97,11 +97,25 @@
     private final MultiPropertyFactory<View> mViewTranslationY;
 
     /**
+     * Gets the view split x-axis translation
+     */
+    public MultiPropertyFactory<View>.MultiProperty getSplitTranslationX() {
+        return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION);
+    }
+
+    /**
      * Sets the view split x-axis translation
      * @param translationX x-axis translation
      */
     public void setSplitTranslationX(float translationX) {
-        mViewTranslationX.get(INDEX_SPLIT_TRANSLATION).setValue(translationX);
+        getSplitTranslationX().setValue(translationX);
+    }
+
+    /**
+     * Gets the view split y-axis translation
+     */
+    public MultiPropertyFactory<View>.MultiProperty getSplitTranslationY() {
+        return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION);
     }
 
     /**
@@ -109,7 +123,7 @@
      * @param translationY y-axis translation
      */
     public void setSplitTranslationY(float translationY) {
-        mViewTranslationY.get(INDEX_SPLIT_TRANSLATION).setValue(translationY);
+        getSplitTranslationY().setValue(translationY);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index a27875a..97f3d81 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -25,6 +25,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 
 import android.annotation.TargetApi;
@@ -90,7 +91,7 @@
         stateManager.goToState(NORMAL, animated);
         if (FeatureFlags.enableSplitContextually()) {
             mSplitSelectStateController.getSplitAnimationController()
-                    .playPlaceholderDismissAnim(mActivity);
+                    .playPlaceholderDismissAnim(mActivity, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
         }
         AbstractFloatingView.closeAllOpenViews(mActivity, animated);
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ecff6b4..99c42a7 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -147,14 +147,15 @@
 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;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
 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;
@@ -589,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,
@@ -4815,7 +4815,8 @@
      *          false otherwise
      */
     public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable,
-            View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user) {
+            View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user,
+            ItemInfo itemInfo) {
         if (canLaunchFullscreenTask()) {
             return false;
         }
@@ -4834,9 +4835,9 @@
                                 + ") is not dockable / does not support splitscreen"));
                 return true;
             }
-            mSplitSelectStateController.setSecondTask(task);
+            mSplitSelectStateController.setSecondTask(task, itemInfo);
         } else {
-            mSplitSelectStateController.setSecondTask(intent, user);
+            mSplitSelectStateController.setSecondTask(intent, user, itemInfo);
         }
 
         RectF secondTaskStartingBounds = new RectF();
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index c4b93b7..a11a913 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.views;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
@@ -39,6 +41,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.quickstep.util.SplitSelectStateController;
 
 /**
  * A rounded rectangular component containing a single TextView.
@@ -130,8 +133,11 @@
     }
 
     private void exitSplitSelection() {
-        ((RecentsView) mLauncher.getOverviewPanel()).getSplitSelectController()
-                .getSplitAnimationController().playPlaceholderDismissAnim(mLauncher);
+        SplitSelectStateController splitSelectController =
+                ((RecentsView) mLauncher.getOverviewPanel()).getSplitSelectController();
+
+        splitSelectController.getSplitAnimationController().playPlaceholderDismissAnim(mLauncher,
+                LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON);
         mLauncher.getStateManager().goToState(LauncherState.NORMAL);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 55da160..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;
@@ -836,7 +833,7 @@
             return getRecentsView().confirmSplitSelect(this, container.getTask(),
                     container.getIconView().getDrawable(), container.getThumbnailView(),
                     container.getThumbnailView().getThumbnail(), /* intent */ null,
-                    /* user */ null);
+                    /* user */ null, container.getItemInfo());
         }
         return false;
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index cbb4894..1bc2c67 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -305,6 +305,7 @@
     @NavigationModeSwitch
     @PortraitLandscape
     @ScreenRecord // b/313464374
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
     public void testQuickSwitchFromApp() throws Exception {
         startTestActivity(2);
         startTestActivity(3);
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
index c6c5be4..b4f1692 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt
@@ -60,6 +60,7 @@
     private val sampleShortcut = Intent()
     private val sampleShortcut2 = Intent()
     private val sampleItemInfo = ItemInfo()
+    private val sampleItemInfo2 = ItemInfo()
     private val samplePackage =
         AbstractLauncherUiTest.resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)
 
@@ -133,7 +134,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2)
         assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
     }
 
@@ -145,7 +146,7 @@
             null,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
     }
 
@@ -158,7 +159,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser, sampleItemInfo2)
         assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed())
     }
 
@@ -170,7 +171,7 @@
             sampleItemInfo,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId2)
+        splitSelectDataHolder.setSecondTask(sampleTaskId2, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_TASK_TASK)
@@ -194,7 +195,7 @@
             sampleItemInfo,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_TASK_PENDINGINTENT)
@@ -218,7 +219,7 @@
             sampleItemInfo,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_TASK_SHORTCUT)
@@ -243,7 +244,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_PENDINGINTENT_TASK)
@@ -268,7 +269,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleTaskId)
+        splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_SHORTCUT_TASK)
@@ -293,7 +294,7 @@
             null,
             INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent2, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent2, sampleUser, sampleItemInfo2)
         val launchData = splitSelectDataHolder.getSplitLaunchData()
 
         assertEquals(launchData.splitLaunchType, SPLIT_PENDINGINTENT_PENDINGINTENT)
@@ -388,7 +389,7 @@
             null,
             null
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         splitSelectDataHolder.resetState()
         assertFalse(splitSelectDataHolder.isSplitSelectActive())
     }
@@ -402,7 +403,7 @@
                 null,
                 INVALID_TASK_ID
         )
-        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser)
+        splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2)
         splitSelectDataHolder.resetState()
         assertFalse(splitSelectDataHolder.isSplitSelectActive())
     }
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 1e39a34..18b1ea0 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -27,6 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.LauncherState
 import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.StatsLogger
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
@@ -45,6 +46,8 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.`when`
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
@@ -56,13 +59,14 @@
     private val systemUiProxy: SystemUiProxy = mock()
     private val depthController: DepthController = mock()
     private val statsLogManager: StatsLogManager = mock()
+    private val statsLogger: StatsLogger = mock()
     private val stateManager: StateManager<LauncherState> = mock()
     private val handler: Handler = mock()
     private val context: StatefulActivity<*> = mock()
     private val recentsModel: RecentsModel = mock()
     private val pendingIntent: PendingIntent = mock()
 
-    lateinit var splitSelectStateController: SplitSelectStateController
+    private lateinit var splitSelectStateController: SplitSelectStateController
 
     private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId)
     private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
@@ -74,6 +78,9 @@
 
     @Before
     fun setup() {
+        `when`(statsLogManager.logger()).thenReturn(statsLogger)
+        `when`(statsLogger.withInstanceId(any())).thenReturn(statsLogger)
+        `when`(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
         splitSelectStateController =
             SplitSelectStateController(
                 context,
@@ -593,9 +600,10 @@
     @Test
     fun secondPendingIntentSet() {
         val itemInfo = ItemInfo()
+        val itemInfo2 = ItemInfo()
         whenever(pendingIntent.creatorUserHandle).thenReturn(primaryUserHandle)
         splitSelectStateController.setInitialTaskSelect(null, 0, itemInfo, null, 1)
-        splitSelectStateController.setSecondTask(pendingIntent)
+        splitSelectStateController.setSecondTask(pendingIntent, itemInfo2)
         assertTrue(splitSelectStateController.isBothSplitAppsConfirmed)
     }
 
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index f72c556..e7b88dc 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -124,7 +124,8 @@
 
     /** Type of popups that should get exclusive accessibility focus. */
     public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER
-            & ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP;
+            & ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP
+            & ~TYPE_WIDGET_RESIZE_FRAME;
 
     // These view all have particular operation associated with swipe down interaction.
     public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET |
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 38ddfec..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;
 
@@ -425,11 +425,13 @@
     }
 
     /**
-     *  Only if actual text can be displayed in two line, the {@code true} value will be effective.
+     * Only if actual text can be displayed in two line, the {@code true} value will be effective.
      */
     protected boolean shouldUseTwoLine() {
-        return (FeatureFlags.enableTwolineAllapps() && isCurrentLanguageEnglish())
-                && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW);
+        return FeatureFlags.enableTwolineAllapps() && isCurrentLanguageEnglish()
+                && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW)
+                && (!Flags.enableTwolineToggle() || (Flags.enableTwolineToggle()
+                && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext())));
     }
 
     protected boolean isCurrentLanguageEnglish() {
@@ -581,12 +583,12 @@
 
     /**
      * Find the appropriate text spacing to display the provided text
-     * @param paint the paint used by the text view
-     * @param text the text to display
-     * @param allowedWidthPx available space to render the text
-     * @param minSpacingEm minimum spacing allowed between characters
-     * @return the final textSpacing value
      *
+     * @param paint          the paint used by the text view
+     * @param text           the text to display
+     * @param allowedWidthPx available space to render the text
+     * @param minSpacingEm   minimum spacing allowed between characters
+     * @return the final textSpacing value
      * @see #setLetterSpacing(float)
      */
     private float findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx,
@@ -814,13 +816,13 @@
      * iterating through the list of break points and determining if the strings between the break
      * points can fit within the line it is in. We will show the modified string if there is enough
      * horizontal and vertical space, otherwise this method will just return the original string.
-     *  Example assuming each character takes up one spot:
-     *  title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7
-     *  We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery,
-     *  now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth
-     *  at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking
-     *  if the first char is a SPACE, we trim to append "Stats". So resulting string would be
-     *  "Battery\nStats"
+     * Example assuming each character takes up one spot:
+     * title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7
+     * We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery,
+     * now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth
+     * at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking
+     * if the first char is a SPACE, we trim to append "Stats". So resulting string would be
+     * "Battery\nStats"
      */
     public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight,
             CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier,
@@ -834,25 +836,25 @@
         StringBuilder newString = new StringBuilder();
         paint.setLetterSpacing(MIN_LETTER_SPACING);
         int stringPtr = 0;
-        for (int i = 0; i < breakPoints.size()+1; i++) {
+        for (int i = 0; i < breakPoints.size() + 1; i++) {
             if (i < breakPoints.size()) {
-                currentWord = title.subSequence(stringPtr, breakPoints.get(i)+1);
+                currentWord = title.subSequence(stringPtr, breakPoints.get(i) + 1);
             } else {
                 // last word from recent breakpoint until the end of the string
                 currentWord = title.subSequence(stringPtr, title.length());
             }
-            currentWordWidth = paint.measureText(currentWord,0, currentWord.length());
+            currentWordWidth = paint.measureText(currentWord, 0, currentWord.length());
             runningWidth += currentWordWidth;
             if (runningWidth <= limitedWidth) {
                 newString.append(currentWord);
             } else {
-                if (i != 0)  {
+                if (i != 0) {
                     // If putting word onto a new line, make sure there is no space or new line
                     // character in the beginning of the current word and just put in the rest of
                     // the characters.
                     CharSequence lastCharacters = title.subSequence(stringPtr, title.length());
                     int beginningLetterType =
-                            Character.getType(Character.codePointAt(lastCharacters,0));
+                            Character.getType(Character.codePointAt(lastCharacters, 0));
                     if (beginningLetterType == Character.SPACE_SEPARATOR
                             || beginningLetterType == Character.LINE_SEPARATOR) {
                         lastCharacters = lastCharacters.length() > 1
@@ -873,7 +875,7 @@
                 // no need to look forward into the string if we've already finished processing
                 break;
             }
-            stringPtr = breakPoints.get(i)+1;
+            stringPtr = breakPoints.get(i) + 1;
         }
         return newString.toString();
     }
@@ -963,12 +965,12 @@
         return preloadDrawable;
     }
 
-    private boolean isIconDisabled(ItemInfoWithIcon info) {
-        if (info.isArchived()) {
-            return info.getProgressLevel() == 0
-                    && (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0;
-        }
-        return info.getProgressLevel() == 0;
+    /**
+     * Returns true to grey the icon if the icon is either suspended or if the icon is pending
+     * download
+     */
+    public boolean isIconDisabled(ItemInfoWithIcon info) {
+        return info.isDisabled() || info.isPendingDownload();
     }
 
     public void applyDotState(ItemInfo itemInfo, boolean animate) {
@@ -1015,12 +1017,12 @@
             if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
                 setContentDescription(getContext()
                         .getString(
-                            R.string.app_installing_title, info.title, percentageString));
+                                R.string.app_installing_title, info.title, percentageString));
             } else if ((info.runtimeStatusFlags
                     & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
                 setContentDescription(getContext()
                         .getString(
-                            R.string.app_downloading_title, info.title, percentageString));
+                                R.string.app_downloading_title, info.title, percentageString));
             }
         }
     }
@@ -1187,7 +1189,8 @@
     public SafeCloseable prepareDrawDragView() {
         resetIconScale();
         setForceHideDot(true);
-        return () -> { };
+        return () -> {
+        };
     }
 
     private void resetIconScale() {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 4afa8e0..4dd2fe3 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -717,7 +717,7 @@
         }
 
         // Calculate all of the remaining variables.
-        extraSpace = updateAvailableDimensions(res);
+        extraSpace = updateAvailableDimensions(context);
 
         calculateAndSetWorkspaceVerticalPadding(context, inv, extraSpace);
 
@@ -1009,14 +1009,14 @@
     /**
      * Returns the amount of extra (or unused) vertical space.
      */
-    private int updateAvailableDimensions(Resources res) {
+    private int updateAvailableDimensions(Context context) {
         iconCenterVertically = (mIsScalableGrid || mIsResponsiveGrid) && isVerticalBarLayout();
 
         if (mIsResponsiveGrid) {
             iconSizePx = mResponsiveWorkspaceCellSpec.getIconSize();
             iconTextSizePx = mResponsiveWorkspaceCellSpec.getIconTextSize();
             mIconDrawablePaddingOriginalPx = mResponsiveWorkspaceCellSpec.getIconDrawablePadding();
-            updateIconSize(1f, res);
+            updateIconSize(1f, context);
             updateWorkspacePadding();
             return 0;
         }
@@ -1026,7 +1026,7 @@
         iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics));
         iconTextSizePx = pxFromSp(invIconTextSizeSp, mMetrics);
 
-        updateIconSize(1f, res);
+        updateIconSize(1f, context);
         updateWorkspacePadding();
 
         // Check to see if the icons fit within the available height.
@@ -1050,7 +1050,7 @@
 
         if (shouldScale) {
             float scale = Math.min(scaleX, scaleY);
-            updateIconSize(scale, res);
+            updateIconSize(scale, context);
             extraHeight = Math.max(0, maxHeight - getCellLayoutHeightSpecification());
         }
 
@@ -1096,7 +1096,7 @@
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
      * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
      */
-    public void updateIconSize(float scale, Resources res) {
+    public void updateIconSize(float scale, Context context) {
         // Icon scale should never exceed 1, otherwise pixellation may occur.
         iconScale = Math.min(1f, scale);
         cellScaleToFit = scale;
@@ -1216,13 +1216,15 @@
         if (mIsResponsiveGrid) {
             updateAllAppsWithResponsiveMeasures();
         } else {
-            updateAllAppsIconSize(scale, res);
+            updateAllAppsIconSize(scale, context.getResources());
         }
         updateAllAppsContainerWidth();
         if (isVerticalLayout && !mIsResponsiveGrid) {
             hideWorkspaceLabelsIfNotEnoughSpace();
         }
-        if (FeatureFlags.enableTwolineAllapps()) {
+        if (FeatureFlags.enableTwolineAllapps()
+                && (!Flags.enableTwolineToggle() || (Flags.enableTwolineToggle()
+                && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context)))) {
             // Add extra textHeight to the existing allAppsCellHeight.
             allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx);
         }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 07eea32..edfef5e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -78,6 +78,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED;
@@ -1622,7 +1623,7 @@
             }
 
             if (FeatureFlags.enableSplitContextually()) {
-                handleSplitAnimationGoingToHome();
+                handleSplitAnimationGoingToHome(LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
             }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
             handleGestureContract(intent);
@@ -1638,7 +1639,7 @@
     }
 
     /** Handle animating away split placeholder view when user taps on home button */
-    protected void handleSplitAnimationGoingToHome() {
+    protected void handleSplitAnimationGoingToHome(EventEnum splitDismissReason) {
         // Overridden
     }
 
@@ -2735,7 +2736,7 @@
     }
 
     /** Call to dismiss the intermediary split selection state. */
-    public void dismissSplitSelection() {
+    public void dismissSplitSelection(StatsLogManager.LauncherEvent splitDismissEvent) {
         // Overridden; move this into ActivityContext if necessary for Taskbar
     }
 
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 3b62ae1..b0a644b 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -349,6 +349,8 @@
         @JvmField
         val PRIVATE_SPACE_APPS =
             nonRestorableItem("pref_private_space_apps", 0, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
+        @JvmField val ENABLE_TWOLINE_ALLAPPS_TOGGLE =
+                backedUpItem("pref_enable_two_line_toggle", false)
         @JvmField
         val THEMED_ICONS =
             backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 0e38007..9867556 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -132,7 +132,7 @@
         launcher.viewCache.setCacheSize(R.layout.folder_page, 2)
         TraceHelper.INSTANCE.endSection()
         launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true)
-        launcher.workspace.pageIndicator.setAreScreensBinding(false, deviceProfile.isTwoPanels)
+        launcher.workspace.pageIndicator.setPauseScroll(/*pause=*/ false, deviceProfile.isTwoPanels)
     }
 
     /**
@@ -299,8 +299,8 @@
     }
 
     override fun bindScreens(orderedScreenIds: LIntArray) {
-        launcher.workspace.pageIndicator.setAreScreensBinding(
-            true,
+        launcher.workspace.pageIndicator.setPauseScroll(
+            /*pause=*/ true,
             launcher.deviceProfile.isTwoPanels
         )
         val firstScreenPosition = 0
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 55416af..ca34dd1 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -2226,8 +2226,7 @@
     private Runnable getWidgetResizeFrameRunnable(DragOptions options,
             LauncherAppWidgetHostView hostView, CellLayout cellLayout) {
         AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
-        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
-                && !options.isAccessibleDrag) {
+        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) {
             return () -> {
                 if (!isPageInTransition()) {
                     AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 27092f6..ca587c1 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -36,6 +36,8 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.config.FeatureFlags;
@@ -221,8 +223,11 @@
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         switch (viewType) {
             case VIEW_TYPE_ICON:
-                int layout = !FeatureFlags.enableTwolineAllapps() ? R.layout.all_apps_icon
-                                : R.layout.all_apps_icon_twoline;
+                int layout = (FeatureFlags.enableTwolineAllapps() &&
+                        (!Flags.enableTwolineToggle() || (Flags.enableTwolineToggle()
+                                && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(
+                                        mActivityContext.getApplicationContext()))))
+                        ? R.layout.all_apps_icon_twoline : R.layout.all_apps_icon;
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                         layout, parent, false);
                 icon.setLongPressTimeoutFactor(1f);
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 871643e..9001a0c 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -119,7 +119,7 @@
             final Runnable onCompleteRunnable) {
         return reverse
                 ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1,
-                        FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable)
+                FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable)
                 : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2,
                         INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable);
     }
@@ -219,9 +219,9 @@
     /**
      * Draws each preview item.
      *
-     * @param offset The offset needed to draw the preview items.
+     * @param offset         The offset needed to draw the preview items.
      * @param shouldClipPath Iff true, clip path using {@param clipPath}.
-     * @param clipPath The clip path of the folder icon.
+     * @param clipPath       The clip path of the folder icon.
      */
     private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
             boolean shouldClipPath, Path clipPath) {
@@ -362,13 +362,13 @@
 
     /**
      * Handles the case where items in the preview are either:
-     *  - Moving into the preview
-     *  - Moving into a new position
-     *  - Moving out of the preview
+     * - Moving into the preview
+     * - Moving into a new position
+     * - Moving out of the preview
      *
      * @param oldItems The list of items in the old preview.
      * @param newItems The list of items in the new preview.
-     * @param dropped The item that was dropped onto the FolderIcon.
+     * @param dropped  The item that was dropped onto the FolderIcon.
      */
     public void onDrop(List<WorkspaceItemInfo> oldItems, List<WorkspaceItemInfo> newItems,
             WorkspaceItemInfo dropped) {
@@ -433,9 +433,8 @@
     @VisibleForTesting
     public void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
         if (item.hasPromiseIconUi() || (item.runtimeStatusFlags
-                    & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
+                & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
             PreloadIconDrawable drawable = newPendingIcon(mContext, item);
-            drawable.setLevel(item.getProgressLevel());
             p.drawable = drawable;
         } else {
             p.drawable = item.newIcon(mContext,
@@ -443,7 +442,6 @@
         }
         p.drawable.setBounds(0, 0, mIconSize, mIconSize);
         p.item = item;
-
         // Set the callback to FolderIcon as it is responsible to drawing the icon. The
         // callback will be released when the folder is opened.
         p.drawable.setCallback(mIcon);
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 3e77c78..9fffcc1 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -173,6 +173,8 @@
         mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);
 
         setLevel(info.getProgressLevel());
+        // Set a disabled icon color if the app is suspended or if the app is pending download
+        setIsDisabled(info.isDisabled() || info.isPendingDownload());
     }
 
     @Override
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/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 2aabb7c..5cb1540 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -199,6 +199,11 @@
         @UiEvent(doc = "User tapped on app info system shortcut.")
         LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP(515),
 
+        /**
+         * @deprecated Use {@link #LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP} or
+         *             {@link #LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM}
+         */
+        @Deprecated
         @UiEvent(doc = "User tapped on split screen icon on a task menu.")
         LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP(518),
 
@@ -725,6 +730,27 @@
         @UiEvent(doc = "User tapped on private space uninstall system shortcut.")
         LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP(1608),
 
+        @UiEvent(doc = "User initiated split selection")
+        LAUNCHER_SPLIT_SELECTION_INITIATED(1618),
+
+        @UiEvent(doc = "User finished a split selection session")
+        LAUNCHER_SPLIT_SELECTION_COMPLETE(1619),
+
+        @UiEvent(doc = "User selected both apps for split screen")
+        LAUNCHER_SPLIT_SELECTED_SECOND_APP(1609),
+
+        @UiEvent(doc = "User exited split selection by going home via swipe, button, or state "
+                + "transition")
+        LAUNCHER_SPLIT_SELECTION_EXIT_HOME(1610),
+
+        @UiEvent(doc = "User exited split selection by tapping cancel in split instructions view")
+        LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON(1611),
+
+        @UiEvent(doc = "User exited split selection when another activity/app came to foreground"
+                + " after first app had been selected OR if user long-pressed on home. Default exit"
+                + " metric.")
+        LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED(1612),
+
         // ADD MORE
         ;
 
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index e46c502..352c363 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -139,7 +139,8 @@
      */
     private int mProgressLevel = 100;
 
-    protected ItemInfoWithIcon() { }
+    protected ItemInfoWithIcon() {
+    }
 
     protected ItemInfoWithIcon(ItemInfoWithIcon info) {
         super(info);
@@ -155,7 +156,20 @@
     }
 
     /**
-     * Returns true if the app corresponding to the item is archived. */
+     * @return {@code true} if the app is pending download (0 progress) or if the app is archived
+     * and its install session is active
+     */
+    public boolean isPendingDownload() {
+        if (isArchived()) {
+            return this.getProgressLevel() == 0
+                    && (this.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0;
+        }
+        return getProgressLevel() == 0;
+    }
+
+    /**
+     * Returns true if the app corresponding to the item is archived.
+     */
     public boolean isArchived() {
         if (!Utilities.enableSupportForArchiving()) {
             return false;
@@ -179,7 +193,7 @@
     public boolean isAppStartable() {
         return ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) == 0)
                 && (((runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0)
-                    || mProgressLevel == 100 || isArchived());
+                || mProgressLevel == 100 || isArchived());
     }
 
     /**
@@ -242,7 +256,7 @@
 
         return targetPackage != null
                 ? ApiWrapper.getAppMarketActivityIntent(
-                        context, targetPackage, Process.myUserHandle())
+                context, targetPackage, Process.myUserHandle())
                 : null;
     }
 
diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java
index 30156c8..0640bf3 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicator.java
@@ -27,10 +27,12 @@
     void setMarkersCount(int numMarkers);
 
     /**
-     * Sets flag to indicate when the screens are in the process of binding so that we don't animate
-     * during that period.
+     * Sets a flag indicating whether to pause scroll.
+     * <p>Should be set to {@code true} while the screen is binding or new data is being applied,
+     * and to {@code false} once done. This prevents animation conflicts due to scrolling during
+     * those periods.</p>
      */
-    default void setAreScreensBinding(boolean areScreensBinding, boolean isTwoPanels) {
+    default void setPauseScroll(boolean pause, boolean isTwoPanels) {
         // No-op by default
     }
 
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index 1b5abaa..77effca 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -130,7 +130,7 @@
      */
     private float mCurrentPosition;
     private float mFinalPosition;
-    private boolean mAreScreensBinding;
+    private boolean mIsScrollPaused;
     private boolean mIsTwoPanels;
     private ObjectAnimator mAnimator;
     private @Nullable ObjectAnimator mAlphaAnimator;
@@ -172,7 +172,7 @@
         }
 
         // Skip scroll update during binding. We will update it when binding completes.
-        if (mAreScreensBinding) {
+        if (mIsScrollPaused) {
             return;
         }
 
@@ -376,15 +376,15 @@
     }
 
     @Override
-    public void setAreScreensBinding(boolean areScreensBinding, boolean isTwoPanels) {
+    public void setPauseScroll(boolean pause, boolean isTwoPanels) {
         mIsTwoPanels = isTwoPanels;
 
         // Reapply correct current position which was skipped during setScroll.
-        if (mAreScreensBinding && !areScreensBinding) {
+        if (mIsScrollPaused && !pause) {
             CURRENT_POSITION.set(this, (float) mActivePage);
         }
 
-        mAreScreensBinding = areScreensBinding;
+        mIsScrollPaused = pause;
     }
 
     @Override
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/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 8c43f75..0ff10c2 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_TAP_OUTSIDE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_LONGPRESS;
 
 import android.graphics.PointF;
@@ -207,7 +208,7 @@
                 mLauncher.getStatsLogManager().logger().log(LAUNCHER_WORKSPACE_LONGPRESS);
                 mLauncher.showDefaultOptions(mTouchDownPoint.x, mTouchDownPoint.y);
                 if (FeatureFlags.enableSplitContextually() && mLauncher.isSplitSelectionActive()) {
-                    mLauncher.dismissSplitSelection();
+                    mLauncher.dismissSplitSelection(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
                 }
             } else {
                 cancelLongPress();
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/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 738d74e..426a3ae 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -29,7 +29,6 @@
 import androidx.annotation.Px;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.model.WidgetItem;
@@ -88,6 +87,7 @@
      * Displays all the provided recommendations in a single table if they fit.
      *
      * @param recommendedWidgets list of widgets to be displayed in recommendation section.
+     * @param deviceProfile      the current {@link DeviceProfile}
      * @param availableHeight    height in px that can be used to display the recommendations;
      *                           recommendations that don't fit in this height won't be shown
      * @param availableWidth     width in px that the recommendations should display in
@@ -96,12 +96,13 @@
      * @return {@code false} if no recommendations could fit in the available space.
      */
     public boolean setRecommendations(
-            List<WidgetItem> recommendedWidgets, final @Px float availableHeight,
-            final @Px int availableWidth, final @Px int cellPadding) {
+            List<WidgetItem> recommendedWidgets, DeviceProfile deviceProfile,
+            final @Px float availableHeight, final @Px int availableWidth,
+            final @Px int cellPadding) {
         this.mAvailableHeight = availableHeight;
         removeAllViews();
 
-        maybeDisplayInTable(recommendedWidgets, availableWidth, cellPadding);
+        maybeDisplayInTable(recommendedWidgets, deviceProfile, availableWidth, cellPadding);
         updateTitleAndIndicator();
         return getChildCount() > 0;
     }
@@ -111,6 +112,7 @@
      * <p>In case of a single category, no title is displayed for it.</p>
      *
      * @param recommendations a map of widget items per recommendation category
+     * @param deviceProfile   the current {@link DeviceProfile}
      * @param availableHeight height in px that can be used to display the recommendations;
      *                        recommendations that don't fit in this height won't be shown
      * @param availableWidth  width in px that the recommendations should display in
@@ -120,10 +122,12 @@
      */
     public boolean setRecommendations(
             Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
+            DeviceProfile deviceProfile,
             final @Px float availableHeight, final @Px int availableWidth,
             final @Px int cellPadding) {
         this.mAvailableHeight = availableHeight;
         Context context = getContext();
+        mPageIndicator.setPauseScroll(true, deviceProfile.isTwoPanels);
         removeAllViews();
 
         int displayedCategories = 0;
@@ -133,7 +137,7 @@
                 new TreeMap<>(recommendations).entrySet()) {
             // If none of the recommendations for the category could fit in the mAvailableHeight, we
             // don't want to add that category; and we look for the next one.
-            if (maybeDisplayInTable(entry.getValue(), availableWidth, cellPadding)) {
+            if (maybeDisplayInTable(entry.getValue(), deviceProfile, availableWidth, cellPadding)) {
                 mCategoryTitles.add(
                         context.getResources().getString(entry.getKey().categoryTitleRes));
                 displayedCategories++;
@@ -145,14 +149,21 @@
         }
 
         updateTitleAndIndicator();
+        mPageIndicator.setPauseScroll(false, deviceProfile.isTwoPanels);
         return getChildCount() > 0;
     }
 
     /** Displays the page title and paging indicator if there are multiple pages. */
     private void updateTitleAndIndicator() {
-        int titleAndIndicatorVisibility = getPageCount() > 1 ? View.VISIBLE : View.GONE;
+        boolean showPaginatedView = getPageCount() > 1;
+        int titleAndIndicatorVisibility = showPaginatedView ? View.VISIBLE : View.GONE;
         mRecommendationPageTitle.setVisibility(titleAndIndicatorVisibility);
         mPageIndicator.setVisibility(titleAndIndicatorVisibility);
+        if (showPaginatedView) {
+            mPageIndicator.setActiveMarker(0);
+            setCurrentPage(0);
+            mRecommendationPageTitle.setText(mCategoryTitles.get(0));
+        }
     }
 
     @Override
@@ -218,9 +229,9 @@
      * <p>Returns false if none of the recommendations could fit.</p>
      */
     private boolean maybeDisplayInTable(List<WidgetItem> recommendedWidgets,
+            DeviceProfile deviceProfile,
             final @Px int availableWidth, final @Px int cellPadding) {
         Context context = getContext();
-        DeviceProfile deviceProfile = Launcher.getLauncher(context).getDeviceProfile();
         LayoutInflater inflater = LayoutInflater.from(context);
 
         List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
@@ -239,7 +250,7 @@
         recommendationsTable.setWidgetCellLongClickListener(mWidgetCellOnLongClickListener);
 
         boolean displayedAtLeastOne = recommendationsTable.setRecommendedWidgets(rows,
-                mAvailableHeight);
+                deviceProfile, mAvailableHeight);
         if (displayedAtLeastOne) {
             addView(recommendationsTable);
         }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 237078e..f5742af 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -590,6 +590,7 @@
         if (enableCategorizedWidgetSuggestions()) {
             mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
                     mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(),
+                    mDeviceProfile,
                     /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
                     /* availableWidth= */ mMaxSpanPerRow,
                     /* cellPadding= */ mWidgetCellHorizontalPadding
@@ -597,6 +598,7 @@
         } else {
             mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
                     mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+                    mDeviceProfile,
                     /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
                     /* availableWidth= */ mMaxSpanPerRow,
                     /* cellPadding= */ mWidgetCellHorizontalPadding
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/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index ce1f4e0..47750a5 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -32,7 +32,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.widget.WidgetCell;
@@ -89,9 +88,11 @@
      * <p>Returns {@code false} if none of the widgets could fit</p>
      */
     public boolean setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
+            DeviceProfile deviceProfile,
             float recommendationTableMaxHeight) {
         mRecommendationTableMaxHeight = recommendationTableMaxHeight;
         RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f,
+                deviceProfile,
                 recommendedWidgets);
         bindData(data);
         return !data.mRecommendationTable.isEmpty();
@@ -144,6 +145,7 @@
 
     private RecommendationTableData fitRecommendedWidgetsToTableSpace(
             float previewScale,
+            DeviceProfile deviceProfile,
             List<ArrayList<WidgetItem>> recommendedWidgetsInTable) {
         if (previewScale < MAX_DOWN_SCALE_RATIO) {
             Log.w(TAG, "Hide recommended widgets. Can't down scale previews to " + previewScale);
@@ -151,7 +153,6 @@
         }
         // A naive estimation of the widgets recommendation table height without inflation.
         float totalHeight = mWidgetsRecommendationTableVerticalPadding;
-        DeviceProfile deviceProfile = Launcher.getLauncher(getContext()).getDeviceProfile();
         for (int i = 0; i < recommendedWidgetsInTable.size(); i++) {
             List<WidgetItem> widgetItems = recommendedWidgetsInTable.get(i);
             float rowHeight = 0;
@@ -175,12 +176,14 @@
             // num of row by 1 to see if it fits.
             return fitRecommendedWidgetsToTableSpace(
                     previewScale,
+                    deviceProfile,
                     recommendedWidgetsInTable.subList(/* fromIndex= */0,
                             /* toIndex= */recommendedWidgetsInTable.size() - 1));
         }
 
         float nextPreviewScale = previewScale * DOWN_SCALE_RATIO;
-        return fitRecommendedWidgetsToTableSpace(nextPreviewScale, recommendedWidgetsInTable);
+        return fitRecommendedWidgetsToTableSpace(nextPreviewScale, deviceProfile,
+                recommendedWidgetsInTable);
     }
 
     /** Data class for the widgets recommendation table and widgets preview scaling. */
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)
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 20ba04b..a5d5884 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -121,6 +121,7 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/326130648
     public void testUninstallFromAllApps() throws Exception {
         installDummyAppAndWaitForUIUpdate();
         try {
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 5283c6b..8631f03 100644
--- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -11,11 +11,11 @@
 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2;
 import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3;
 import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 import static com.android.launcher3.util.TestUtil.DUMMY_CLASS_NAME;
 import static com.android.launcher3.util.TestUtil.DUMMY_PACKAGE;
 import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -40,13 +40,14 @@
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
 import java.io.IOException;
@@ -61,6 +62,9 @@
 @RunWith(AndroidJUnit4.class)
 public class CacheDataUpdatedTaskTest {
 
+    @Rule(order = 0)
+    public TestRule testStabilityRule = new TestStabilityRule();
+
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
@@ -157,6 +161,7 @@
     }
 
     @Test
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325283522
     public void testSessionUpdate_updates_pending_apps() {
         // Run on model executor so that no other task runs in the middle.
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
diff --git a/tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt b/tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt
new file mode 100644
index 0000000..cfbde98
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/data/ItemInfoWithIconTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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.model.data
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.pm.PackageInstallInfo
+import com.android.launcher3.util.LauncherModelHelper
+import com.google.common.truth.Truth
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ItemInfoWithIconTest {
+
+    private var context = LauncherModelHelper.SandboxModelContext()
+    private lateinit var itemInfoWithIcon: ItemInfoWithIcon
+
+    @Before
+    fun setup() {
+        itemInfoWithIcon =
+            object : ItemInfoWithIcon() {
+                override fun clone(): ItemInfoWithIcon? {
+                    return null
+                }
+            }
+    }
+
+    @After
+    fun tearDown() {
+        context.destroy()
+    }
+
+    @Test
+    fun itemInfoWithIconDefaultParamsTest() {
+        Truth.assertThat(itemInfoWithIcon.isDisabled).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isPendingDownload).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isArchived).isFalse()
+    }
+
+    @Test
+    fun isDisabledOrPendingTest() {
+        itemInfoWithIcon.setProgressLevel(0, PackageInstallInfo.STATUS_INSTALLING)
+        Truth.assertThat(itemInfoWithIcon.isDisabled).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isPendingDownload).isTrue()
+
+        itemInfoWithIcon.setProgressLevel(1, PackageInstallInfo.STATUS_INSTALLING)
+        Truth.assertThat(itemInfoWithIcon.isDisabled).isFalse()
+        Truth.assertThat(itemInfoWithIcon.isPendingDownload).isFalse()
+    }
+}