Merge "Make Taskbar Divider PopUp Menu Width Dynamic" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 3d15e77..ff97b22 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,6 +1,13 @@
+[Builtin Hooks]
+ktfmt = true
+
+[Builtin Hooks Options]
+ktfmt = --kotlinlang-style
+
+[Tool Paths]
+ktfmt = ${REPO_ROOT}/prebuilts/build-tools/common/framework/ktfmt.jar
+
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
 
-ktfmt_hook = ${REPO_ROOT}/external/ktfmt/ktfmt.py --check ${PREUPLOAD_FILES}
-
 flag_hook = ${REPO_ROOT}/frameworks/base/packages/SystemUI/flag_check.py --msg=${PREUPLOAD_COMMIT_MESSAGE} --files=${PREUPLOAD_FILES} --project=${REPO_PATH}
diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index b243922..b98eee6 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -45,8 +45,11 @@
 
 
 flag {
-    name: "private_space_floating_mask_view"
+    name: "private_space_add_floating_mask_view"
     namespace: "launcher_search"
     description: "This flag enables the floating mask view as part of the Private Space animation. "
     bug: "339850589"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
 }
diff --git a/go/quickstep/res/layout/overview_actions_container.xml b/go/quickstep/res/layout/overview_actions_container.xml
index b1a6202..e31f462 100644
--- a/go/quickstep/res/layout/overview_actions_container.xml
+++ b/go/quickstep/res/layout/overview_actions_container.xml
@@ -124,23 +124,15 @@
     </LinearLayout>
 
     <!-- Unused. Included only for compatibility with parent class. -->
-    <LinearLayout
-        android:id="@+id/group_action_buttons"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/overview_actions_height"
+    <Button
+        android:id="@+id/action_save_app_pair"
+        style="@style/GoOverviewActionButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:layout_gravity="top|center_horizontal"
-        android:orientation="horizontal"
-        android:visibility="gone">
-
-        <Button
-            android:id="@+id/action_save_app_pair"
-            style="@style/GoOverviewActionButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:drawableStart="@drawable/ic_save_app_pair_up_down"
-            android:text="@string/action_save_app_pair"
-            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
-
-    </LinearLayout>
+        android:drawableStart="@drawable/ic_save_app_pair_up_down"
+        android:text="@string/action_save_app_pair"
+        android:theme="@style/ThemeControlHighlightWorkspaceColor"
+        android:visibility="gone" />
 
 </com.android.quickstep.views.GoOverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index 7aaf744..fcd2e54 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -47,22 +47,16 @@
 
     </LinearLayout>
 
-    <LinearLayout
-        android:id="@+id/group_action_buttons"
+    <!-- Currently, the only "group action button" is this save app pair button. If more are added,
+    a new LinearLayout may be needed to contain them, but beware of increased memory usage. -->
+    <Button
+        android:id="@+id/action_save_app_pair"
+        style="@style/OverviewActionButton"
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/overview_actions_height"
+        android:layout_height="wrap_content"
+        android:text="@string/action_save_app_pair"
+        android:theme="@style/ThemeControlHighlightWorkspaceColor"
         android:layout_gravity="bottom|center_horizontal"
-        android:orientation="horizontal"
-        android:visibility="gone">
-
-        <Button
-            android:id="@+id/action_save_app_pair"
-            style="@style/OverviewActionButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/action_save_app_pair"
-            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
-
-    </LinearLayout>
+        android:visibility="gone" />
 
 </com.android.quickstep.views.OverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar_divider_popup_menu.xml b/quickstep/res/layout/taskbar_divider_popup_menu.xml
index 676da15..b184191 100644
--- a/quickstep/res/layout/taskbar_divider_popup_menu.xml
+++ b/quickstep/res/layout/taskbar_divider_popup_menu.xml
@@ -52,7 +52,6 @@
             android:layout_height="wrap_content"
             android:id="@+id/taskbar_pinning_switch"
             android:background="@null"
-            android:clickable="false"
             android:gravity="start|center_vertical"
             android:textAlignment="viewStart"
             android:paddingStart="12dp"
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 3ef8e54..0000c0d 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -30,6 +30,7 @@
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.views.DesktopTaskView
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
 import java.util.function.Consumer
 
 /** Manage recents related operations with desktop tasks */
@@ -57,8 +58,8 @@
     }
 
     /** Launch desktop tasks from recents view */
-    fun moveToDesktop(taskId: Int) {
-        systemUiProxy.moveToDesktop(taskId)
+    fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) {
+        systemUiProxy.moveToDesktop(taskId, transitionSource)
     }
 
     private class RemoteDesktopLaunchTransitionRunner(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index 10103d8..a635537 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
@@ -96,16 +96,21 @@
         popupContainer.getDescendantRectRelativeToSelf(dividerView, outPos)
     }
 
-    @SuppressLint("UseSwitchCompatOrMaterialCode")
+    @SuppressLint("UseSwitchCompatOrMaterialCode", "ClickableViewAccessibility")
     override fun onFinishInflate() {
         super.onFinishInflate()
         val taskbarSwitchOption = requireViewById<LinearLayout>(R.id.taskbar_switch_option)
         val alwaysShowTaskbarSwitch = requireViewById<Switch>(R.id.taskbar_pinning_switch)
         val taskbarVisibilityIcon = requireViewById<View>(R.id.taskbar_pinning_visibility_icon)
+
         alwaysShowTaskbarSwitch.isChecked = alwaysShowTaskbarOn
+        alwaysShowTaskbarSwitch.setOnTouchListener { view, event ->
+            (view.parent as View).onTouchEvent(event)
+        }
+        alwaysShowTaskbarSwitch.setOnClickListener { view -> (view.parent as View).performClick() }
+
         if (ActivityContext.lookupContext<TaskbarActivityContext>(context).isGestureNav) {
             taskbarSwitchOption.setOnClickListener {
-                alwaysShowTaskbarSwitch.isClickable = true
                 alwaysShowTaskbarSwitch.isChecked = !alwaysShowTaskbarOn
                 onClickAlwaysShowTaskbarSwitchOption()
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 74d2d60..8fdb460 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -100,7 +100,10 @@
     // If we're in an app and any of these flags are enabled, taskbar should be stashed.
     private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_SYSUI
             | FLAG_STASHED_IN_APP_SETUP | FLAG_STASHED_IN_TASKBAR_ALL_APPS
-            | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO;
+            | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO | FLAG_STASHED_IME;
+
+    // If we're in overview and any of these flags are enabled, taskbar should be stashed.
+    private static final int FLAGS_STASHED_IN_OVERVIEW = FLAG_STASHED_IME;
 
     // If any of these flags are enabled, inset apps by our stashed height instead of our unstashed
     // height. This way the reported insets are consistent even during transitions out of the app.
@@ -111,7 +114,7 @@
 
     // If any of these flags are enabled, the taskbar must be stashed.
     private static final int FLAGS_FORCE_STASHED = FLAG_STASHED_SYSUI | FLAG_STASHED_DEVICE_LOCKED
-            | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IME;
+            | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN;
 
     /**
      * How long to stash/unstash when manually invoked via long press.
@@ -244,8 +247,13 @@
         boolean inApp = hasAnyFlag(flags, FLAGS_IN_APP);
         boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP);
         boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE);
+        boolean inOverview = hasAnyFlag(flags, FLAG_IN_OVERVIEW);
+        boolean stashedInOverview = hasAnyFlag(flags, FLAGS_STASHED_IN_OVERVIEW);
         boolean forceStashed = hasAnyFlag(flags, FLAGS_FORCE_STASHED);
-        return (inApp && stashedInApp) || (!inApp && stashedLauncherState) || forceStashed;
+        return (inApp && stashedInApp)
+                || (!inApp && stashedLauncherState)
+                || (inOverview && stashedInOverview)
+                || forceStashed;
     };
     private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder(
             mIsStashedPredicate);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index f614dc6..45a9fa1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -98,7 +98,6 @@
         mBarView = barView;
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
-        mBubbleBarAlpha.setUpdateVisibility(true);
         mIconSize = activity.getResources().getDimensionPixelSize(
                 R.dimen.bubblebar_icon_size);
     }
@@ -408,7 +407,19 @@
             b.getView().setOnClickListener(mBubbleClickListener);
             mBubbleDragController.setupBubbleView(b.getView());
 
+            if (b instanceof BubbleBarOverflow) {
+                return;
+            }
+
             if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) {
+                // the bubble bar and handle are initialized as part of the first bubble animation.
+                // if the animation is suppressed, immediately stash or show the bubble bar to
+                // ensure they've been initialized.
+                if (mTaskbarStashController.isInApp()) {
+                    mBubbleStashController.stashBubbleBarImmediate();
+                } else {
+                    mBubbleStashController.showBubbleBarImmediate();
+                }
                 return;
             }
             animateBubbleNotification(bubble, isExpanding);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index 0e6fa3c..a6096e2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -169,7 +169,8 @@
 
     private void setupMagnetizedObject(@NonNull View magnetizedView) {
         mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
-                magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
+                magnetizedView, BubbleDragController.DRAG_TRANSLATION_X,
+                DynamicAnimation.TRANSLATION_Y) {
             @Override
             public float getWidth(@NonNull View underlyingObject) {
                 return underlyingObject.getWidth() * underlyingObject.getScaleX();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
index 287e906..7aed2d2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -60,7 +60,6 @@
     private final float mBubbleFocusedScale;
     private final float mBubbleCapturedScale;
     private final float mDismissCapturedScale;
-    private final FloatPropertyCompat<View> mTranslationXProperty;
 
     /**
      * Should be initialised for each dragged view
@@ -82,28 +81,9 @@
         if (view instanceof BubbleBarView) {
             mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED;
             mBubbleCapturedScale = mDismissCapturedScale;
-            mTranslationXProperty = DynamicAnimation.TRANSLATION_X;
         } else {
             mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED;
             mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED;
-            // Wrap BubbleView.DRAG_TRANSLATION_X as it can't be cast to FloatPropertyCompat<View>
-            mTranslationXProperty = new FloatPropertyCompat<>(
-                    BubbleView.DRAG_TRANSLATION_X.getName()) {
-                @Override
-                public float getValue(View object) {
-                    if (object instanceof BubbleView bubbleView) {
-                        return BubbleView.DRAG_TRANSLATION_X.get(bubbleView);
-                    }
-                    return 0;
-                }
-
-                @Override
-                public void setValue(View object, float value) {
-                    if (object instanceof BubbleView bubbleView) {
-                        BubbleView.DRAG_TRANSLATION_X.setValue(bubbleView, value);
-                    }
-                }
-            };
         }
     }
 
@@ -140,7 +120,7 @@
         mBubbleAnimator
                 .spring(DynamicAnimation.SCALE_X, 1f)
                 .spring(DynamicAnimation.SCALE_Y, 1f)
-                .spring(mTranslationXProperty, restingPosition.x, velocity.x,
+                .spring(BubbleDragController.DRAG_TRANSLATION_X, restingPosition.x, velocity.x,
                         mTranslationConfig)
                 .spring(DynamicAnimation.TRANSLATION_Y, restingPosition.y, velocity.y,
                         mTranslationConfig)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 2ebc3e8..fbd1b88 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
 
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener;
@@ -38,6 +39,37 @@
  * Restores initial position of dragged view if released outside of the dismiss target.
  */
 public class BubbleDragController {
+
+    /**
+     * Property to update dragged bubble x-translation value.
+     * <p>
+     * When applied to {@link BubbleView}, will use set the translation through
+     * {@link BubbleView#getDragTranslationX()} and {@link BubbleView#setDragTranslationX(float)}
+     * methods.
+     * <p>
+     * When applied to {@link BubbleBarView}, will use {@link View#getTranslationX()} and
+     * {@link View#setTranslationX(float)}.
+     */
+    public static final FloatPropertyCompat<View> DRAG_TRANSLATION_X = new FloatPropertyCompat<>(
+            "dragTranslationX") {
+        @Override
+        public float getValue(View view) {
+            if (view instanceof BubbleView bubbleView) {
+                return bubbleView.getDragTranslationX();
+            }
+            return view.getTranslationX();
+        }
+
+        @Override
+        public void setValue(View view, float value) {
+            if (view instanceof BubbleView bubbleView) {
+                bubbleView.setDragTranslationX(value);
+            } else {
+                view.setTranslationX(value);
+            }
+        }
+    };
+
     private final TaskbarActivityContext mActivity;
     private BubbleBarController mBubbleBarController;
     private BubbleBarViewController mBubbleBarViewController;
@@ -62,8 +94,10 @@
         mBubblePinController = bubbleControllers.bubblePinController;
         mBubbleDismissController.setListener(
                 stuck -> {
-                    mBubbleBarPinController.setDropTargetHidden(stuck);
-                    mBubblePinController.setDropTargetHidden(stuck);
+                    if (stuck) {
+                        mBubbleBarPinController.onStuckToDismissTarget();
+                        mBubblePinController.onStuckToDismissTarget();
+                    }
                 });
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 5d01b9b..74ddf90 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -418,6 +418,7 @@
     /** Stashes the bubble bar immediately without animation. */
     public void stashBubbleBarImmediate() {
         mHandleViewController.setTranslationYForSwipe(0);
+        mBubbleStashedHandleAlpha.setValue(1);
         mIconAlphaForStash.setValue(0);
         mIconTranslationYForStash.updateValue(getStashTranslation());
         mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 61a6bce..2e37dc7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -22,13 +22,11 @@
 import android.graphics.Outline;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.FloatProperty;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.widget.ImageView;
 
-import androidx.annotation.NonNull;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.launcher3.R;
@@ -49,25 +47,6 @@
     public static final int DEFAULT_PATH_SIZE = 100;
 
     /**
-     * Property to update drag translation value.
-     *
-     * @see BubbleView#getDragTranslationX()
-     * @see BubbleView#setDragTranslationX(float)
-     */
-    public static final FloatProperty<BubbleView> DRAG_TRANSLATION_X = new FloatProperty<>(
-            "dragTranslationX") {
-        @Override
-        public void setValue(@NonNull BubbleView bubbleView, float value) {
-            bubbleView.setDragTranslationX(value);
-        }
-
-        @Override
-        public Float get(BubbleView bubbleView) {
-            return bubbleView.getDragTranslationX();
-        }
-    };
-
-    /**
      * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
      * another. If any of these flags are set, the dot will not be shown.
      * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 046dbd5..e3a2bab 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -756,6 +756,9 @@
 
     @Override
     public boolean isSplitSelectionActive() {
+        if (mSplitSelectStateController == null) {
+            return false;
+        }
         return mSplitSelectStateController.isSplitSelectActive();
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 2625919..fa80dc2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -102,6 +102,11 @@
     }
 
     @Override
+    public int getTitle() {
+        return R.string.all_apps_label;
+    }
+
+    @Override
     public float getVerticalProgress(Launcher launcher) {
         return 0f;
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 7173298..6822f1b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -182,6 +182,11 @@
         return launcher.getString(R.string.accessibility_recent_apps);
     }
 
+    @Override
+    public int getTitle() {
+        return R.string.accessibility_recent_apps;
+    }
+
     public static float getDefaultSwipeHeight(Launcher launcher) {
         return LayoutUtils.getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
     }
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index 50a06fc..9c188f3 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -24,6 +24,7 @@
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskView.TaskContainer
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.DesktopModeStatus
 
 /** A menu item, "Desktop", that allows the user to bring the current app into Desktop Windowing. */
@@ -43,7 +44,10 @@
     override fun onClick(view: View) {
         dismissTaskMenuView()
         val recentsView = mTarget.getOverviewPanel<RecentsView<*, *>>()
-        recentsView.moveTaskToDesktop(taskContainer) {
+        recentsView.moveTaskToDesktop(
+            taskContainer,
+            DesktopModeTransitionSource.APP_FROM_OVERVIEW
+        ) {
             mTarget.statsLogManager
                 .logger()
                 .withItemInfo(taskContainer.itemInfo)
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 811b9fd..08c2e1c 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -308,7 +308,8 @@
             return;
         }
         LauncherOverlayManager om = launcher.getOverlayManager();
-        if (!launcher.isStarted() || launcher.isForceInvisible()) {
+        if (!SystemUiProxy.INSTANCE.get(launcher).getHomeVisibilityState().isHomeVisible()
+                || launcher.isForceInvisible()) {
             om.hideOverlay(false /* animate */);
         } else {
             om.hideOverlay(150);
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 3091e3d..26a7c2f 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -28,9 +28,11 @@
 import android.graphics.PointF;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.util.Log;
 import android.view.View;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
@@ -58,6 +60,7 @@
  * Helper class to handle various atomic commands for switching between Overview.
  */
 public class OverviewCommandHelper {
+    private static final String TAG = "OverviewCommandHelper";
 
     public static final int TYPE_SHOW = 1;
     public static final int TYPE_KEYBOARD_INPUT = 2;
@@ -105,10 +108,19 @@
      * Called when the command finishes execution.
      */
     private void scheduleNextTask(CommandInfo command) {
-        if (!mPendingCommands.isEmpty() && mPendingCommands.get(0) == command) {
-            mPendingCommands.remove(0);
-            executeNext();
+        if (mPendingCommands.isEmpty()) {
+            Log.d(TAG, "no pending commands to schedule");
+            return;
         }
+        if (mPendingCommands.get(0) != command) {
+            Log.d(TAG, "next task not scheduled."
+                    + " mPendingCommands[0] type is " + mPendingCommands.get(0)
+                    + " - command type is: " + command);
+            return;
+        }
+        Log.d(TAG, "scheduleNextTask called: " + command);
+        mPendingCommands.remove(0);
+        executeNext();
     }
 
     /**
@@ -119,10 +131,14 @@
     @UiThread
     private void executeNext() {
         if (mPendingCommands.isEmpty()) {
+            Log.d(TAG, "executeNext - mPendingCommands is empty");
             return;
         }
         CommandInfo cmd = mPendingCommands.get(0);
-        if (executeCommand(cmd)) {
+
+        boolean result = executeCommand(cmd);
+        Log.d(TAG, "executeNext cmd type: " + cmd + ", result: " + result);
+        if (result) {
             scheduleNextTask(cmd);
         }
     }
@@ -144,14 +160,18 @@
     @BinderThread
     public void addCommand(int type) {
         if (mPendingCommands.size() >= MAX_QUEUE_SIZE) {
+            Log.d(TAG, "the pending command queue is full (" + mPendingCommands.size() + "). "
+                    + "command not added: " + type);
             return;
         }
+        Log.d(TAG, "adding command type: " + type);
         CommandInfo cmd = new CommandInfo(type);
         MAIN_EXECUTOR.execute(() -> addCommand(cmd));
     }
 
     @UiThread
     public void clearPendingCommands() {
+        Log.d(TAG, "clearing pending commands - size: " + mPendingCommands.size());
         mPendingCommands.clear();
     }
 
@@ -182,9 +202,11 @@
 
         if (callbackList != null) {
             callbackList.add(() -> {
+                Log.d(TAG, "launching task callback: " + cmd);
                 scheduleNextTask(cmd);
                 mWaitForToggleCommandComplete = false;
             });
+            Log.d(TAG, "launching task - waiting for callback: " + cmd);
             return false;
         } else {
             recents.startHome();
@@ -200,12 +222,17 @@
     private <T extends StatefulActivity<?> & RecentsViewContainer> boolean executeCommand(
             CommandInfo cmd) {
         if (mWaitForToggleCommandComplete && cmd.type == TYPE_TOGGLE) {
+            Log.d(TAG, "executeCommand: " + cmd
+                    + " - waiting for toggle command complete");
             return true;
         }
         BaseActivityInterface<?, T> activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
         RecentsView visibleRecentsView = activityInterface.getVisibleRecentsView();
         RecentsView createdRecentsView;
+
+        Log.d(TAG, "executeCommand: " + cmd
+                + " - visibleRecentsView: " + visibleRecentsView);
         if (visibleRecentsView == null) {
             T activity = activityInterface.getCreatedContainer();
             createdRecentsView = activity == null ? null : activity.getOverviewPanel();
@@ -292,14 +319,17 @@
                 updateRecentsViewFocus(cmd);
                 logShowOverviewFrom(cmd.type);
             }
+
             @Override
             public void onAnimationEnd(Animator animation) {
+                Log.d(TAG, "switching to Overview state - onAnimationEnd: " + cmd);
                 super.onAnimationEnd(animation);
                 onRecentsViewFocusUpdated(cmd);
                 scheduleNextTask(cmd);
             }
         };
         if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
+            Log.d(TAG, "switching to Overview state - waiting: " + cmd);
             // If successfully switched, wait until animation finishes
             return false;
         }
@@ -366,10 +396,12 @@
             cmd.mActiveCallbacks.addListener(recentAnimListener);
         }
         Trace.beginAsyncSection(TRANSITION_NAME, 0);
+        Log.d(TAG, "switching via recents animation - onGestureStarted: " + cmd);
         return false;
     }
 
     private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
+        Log.d(TAG, "switching via recents animation - onTransitionComplete: " + cmd);
         cmd.removeListener(handler);
         Trace.endAsyncSection(TRANSITION_NAME, 0);
         onRecentsViewFocusUpdated(cmd);
@@ -478,5 +510,15 @@
                 mActiveCallbacks.removeListener(listener);
             }
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return "CommandInfo("
+                    + "type=" + type + ", "
+                    + "createTime=" + createTime + ", "
+                    + "mActiveCallbacks=" + mActiveCallbacks
+                    + ")";
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index dfbae65..b4b8c5b 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -198,6 +198,12 @@
                             .unstashBubbleBarIfStashed();
                 });
                 return response;
+            case TestProtocol.REQUEST_INJECT_FAKE_TRACKPAD:
+                runOnTISBinder(tisBinder -> tisBinder.injectFakeTrackpadForTesting());
+                return response;
+            case TestProtocol.REQUEST_EJECT_FAKE_TRACKPAD:
+                runOnTISBinder(tisBinder -> tisBinder.ejectFakeTrackpadForTesting());
+                return response;
         }
 
         return super.call(method, arg, extras);
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 97a0b3f..3df62da 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -371,6 +371,9 @@
         getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
                 Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
         ACTIVITY_TRACKER.handleCreate(this);
+
+        // Set screen title for Talkback
+        setTitle(R.string.accessibility_recent_apps);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 0ac3ec7..9c3fbaa 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -85,6 +85,7 @@
 import com.android.wm.shell.bubbles.IBubbles;
 import com.android.wm.shell.bubbles.IBubblesListener;
 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.common.pip.IPip;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
@@ -1494,10 +1495,10 @@
     }
 
     /** Call shell to move a task with given `taskId` to desktop  */
-    public void moveToDesktop(int taskId) {
+    public void moveToDesktop(int taskId, DesktopModeTransitionSource transitionSource) {
         if (mDesktopMode != null) {
             try {
-                mDesktopMode.moveToDesktop(taskId);
+                mDesktopMode.moveToDesktop(taskId, transitionSource);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call moveToDesktop", e);
             }
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 4b5c826..fd141c3 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -21,6 +21,7 @@
 import static android.view.Surface.ROTATION_0;
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.window.flags.Flags.enableDesktopWindowingMode;
 
 import android.app.ActivityOptions;
@@ -107,10 +108,14 @@
         public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
                 TaskContainer taskContainer) {
             TaskView taskView = taskContainer.getTaskView();
+            int actionId = taskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT
+                    ? R.id.action_app_info_bottom_right
+                    : R.id.action_app_info_top_left;
+
             AppInfo.SplitAccessibilityInfo accessibilityInfo =
                     new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(),
                             TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()),
-                            taskContainer.getA11yNodeId()
+                            actionId
                     );
             return Collections.singletonList(new AppInfo(container, taskContainer.getItemInfo(),
                     taskView, accessibilityInfo));
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 4599f18..58bb8fc 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -68,11 +68,13 @@
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.graphics.Region;
+import android.hardware.input.InputManager;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.util.ArraySet;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.InputDevice;
@@ -83,6 +85,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ConstantItem;
@@ -146,6 +149,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -396,6 +400,25 @@
             return tis.mTaskbarManager;
         }
 
+        @VisibleForTesting
+        public void injectFakeTrackpadForTesting() {
+            TouchInteractionService tis = mTis.get();
+            if (tis == null) return;
+            tis.mTrackpadsConnected.add(1000);
+            tis.initInputMonitor("tapl testing");
+        }
+
+        @VisibleForTesting
+        public void ejectFakeTrackpadForTesting() {
+            TouchInteractionService tis = mTis.get();
+            if (tis == null) return;
+            tis.mTrackpadsConnected.clear();
+            // This method destroys the current input monitor if set up, and only init a new one
+            // in 3-button mode if {@code mTrackpadsConnected} is not empty. So in other words,
+            // it will destroy the input monitor.
+            tis.initInputMonitor("tapl testing");
+        }
+
         /**
          * Sets whether a predictive back-to-home animation is in progress in the device state
          */
@@ -453,6 +476,47 @@
         }
     }
 
+    private final InputManager.InputDeviceListener mInputDeviceListener =
+            new InputManager.InputDeviceListener() {
+                @Override
+                public void onInputDeviceAdded(int deviceId) {
+                    if (isTrackpadDevice(deviceId)) {
+                        boolean wasEmpty = mTrackpadsConnected.isEmpty();
+                        mTrackpadsConnected.add(deviceId);
+                        if (wasEmpty) {
+                            update();
+                        }
+                    }
+                }
+
+                @Override
+                public void onInputDeviceChanged(int deviceId) {
+                }
+
+                @Override
+                public void onInputDeviceRemoved(int deviceId) {
+                    mTrackpadsConnected.remove(deviceId);
+                    if (mTrackpadsConnected.isEmpty()) {
+                        update();
+                    }
+                }
+
+                private void update() {
+                    if (mInputMonitorCompat != null && !mTrackpadsConnected.isEmpty()) {
+                        // Don't destroy and reinitialize input monitor due to trackpad
+                        // connecting when it's already set up.
+                        return;
+                    }
+                    initInputMonitor("onTrackpadConnected()");
+                }
+
+                private boolean isTrackpadDevice(int deviceId) {
+                    InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
+                    return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE
+                            | InputDevice.SOURCE_TOUCHPAD);
+                }
+            };
+
     private static boolean sConnected = false;
     private static boolean sIsInitialized = false;
     private RotationTouchHelper mRotationTouchHelper;
@@ -503,6 +567,8 @@
     private TaskbarManager mTaskbarManager;
     private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
     private AllAppsActionManager mAllAppsActionManager;
+    private InputManager mInputManager;
+    private final Set<Integer> mTrackpadsConnected = new ArraySet<>();
 
     @Override
     public void onCreate() {
@@ -514,6 +580,15 @@
         mDeviceState = new RecentsAnimationDeviceState(this, true);
         mAllAppsActionManager = new AllAppsActionManager(
                 this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent);
+        mInputManager = getSystemService(InputManager.class);
+        if (ENABLE_TRACKPAD_GESTURE.get()) {
+            mInputManager.registerInputDeviceListener(mInputDeviceListener,
+                    UI_HELPER_EXECUTOR.getHandler());
+            int [] inputDevices = mInputManager.getInputDeviceIds();
+            for (int inputDeviceId : inputDevices) {
+                mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
+            }
+        }
         mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks);
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
@@ -542,7 +617,8 @@
     private void initInputMonitor(String reason) {
         disposeEventHandlers("Initializing input monitor due to: " + reason);
 
-        if (mDeviceState.isButtonNavMode() && !ENABLE_TRACKPAD_GESTURE.get()) {
+        if (mDeviceState.isButtonNavMode() && (!ENABLE_TRACKPAD_GESTURE.get()
+                || mTrackpadsConnected.isEmpty())) {
             return;
         }
 
@@ -678,6 +754,9 @@
 
         mAllAppsActionManager.onDestroy();
 
+        mInputManager.unregisterInputDeviceListener(mInputDeviceListener);
+        mTrackpadsConnected.clear();
+
         mTaskbarManager.destroy();
         sConnected = false;
 
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
new file mode 100644
index 0000000..c1eef0b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.quickstep.recents.data
+
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.flow.Flow
+
+interface RecentTasksRepository {
+    /** Gets all the recent tasks, refreshing from data sources if [forceRefresh] is true. */
+    fun getAllTaskData(forceRefresh: Boolean = false): Flow<List<Task>>
+
+    /**
+     * Gets the data associated with a task that has id [taskId]. Flow will settle on null if the
+     * task was not found.
+     */
+    fun getTaskDataById(taskId: Int): Flow<Task?>
+
+    /**
+     * Sets the tasks that are visible, indicating that properties relating to visuals need to be
+     * populated e.g. icons/thumbnails etc.
+     */
+    fun setVisibleTasks(visibleTaskIdList: List<Int>)
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index ad8ae20..b21a1b4 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -38,7 +38,7 @@
     private val recentsModel: RecentTasksDataSource,
     private val taskThumbnailDataSource: TaskThumbnailDataSource,
     private val taskIconCache: TaskIconCache,
-) {
+) : RecentTasksRepository {
     private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
     private val _taskData =
         groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
@@ -53,17 +53,17 @@
             tasks
         }
 
-    fun getAllTaskData(forceRefresh: Boolean = false): Flow<List<Task>> {
+    override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
         if (forceRefresh) {
             recentsModel.getTasks { groupedTaskData.value = it }
         }
         return taskData
     }
 
-    fun getTaskDataById(taskId: Int): Flow<Task?> =
+    override fun getTaskDataById(taskId: Int): Flow<Task?> =
         taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
 
-    fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         this.visibleTaskIds.value = visibleTaskIdList.toSet()
     }
 
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 0843ae3..40f9b28 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -16,11 +16,19 @@
 
 package com.android.quickstep.task.thumbnail
 
-import com.android.systemui.shared.recents.model.Task
+import android.graphics.Bitmap
+import android.graphics.Rect
+import androidx.annotation.ColorInt
 
 sealed class TaskThumbnailUiState {
     data object Uninitialized : TaskThumbnailUiState()
     data object LiveTile : TaskThumbnailUiState()
+    data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
+    data class Snapshot(
+        val bitmap: Bitmap,
+        val drawnRect: Rect,
+        @ColorInt val backgroundColor: Int
+    ) : TaskThumbnailUiState()
 }
 
-data class TaskThumbnail(val task: Task, val isRunning: Boolean)
+data class TaskThumbnail(val taskId: Int, val isRunning: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 8762976..2836c89 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -19,15 +19,20 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Outline
 import android.graphics.Paint
 import android.graphics.PorterDuff
 import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
 import android.util.AttributeSet
 import android.view.View
 import android.view.ViewOutlineProvider
+import androidx.annotation.ColorInt
 import com.android.launcher3.Utilities
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.util.TaskCornerRadius
 import com.android.quickstep.views.RecentsView
@@ -42,17 +47,26 @@
     //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
     //  This is using a lazy for now because the dependencies cannot be obtained without DI.
     val viewModel by lazy {
-        TaskThumbnailViewModel(
+        val recentsView =
             RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
                 .getOverviewPanel<RecentsView<*, *>>()
-                .mRecentsViewData,
-            (parent as TaskView).taskViewData
+        TaskThumbnailViewModel(
+            recentsView.mRecentsViewData,
+            (parent as TaskView).taskViewData,
+            recentsView.mTasksRepository,
         )
     }
 
     private var uiState: TaskThumbnailUiState = Uninitialized
     private var inheritedScale: Float = 1f
 
+    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+    private val _measuredBounds = Rect()
+    private val measuredBounds: Rect
+        get() {
+            _measuredBounds.set(0, 0, measuredWidth, measuredHeight)
+            return _measuredBounds
+        }
     private var cornerRadius: Float = TaskCornerRadius.get(context)
     private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
 
@@ -85,24 +99,25 @@
         outlineProvider =
             object : ViewOutlineProvider() {
                 override fun getOutline(view: View, outline: Outline) {
-                    outline.setRoundRect(
-                        0,
-                        0,
-                        view.measuredWidth,
-                        view.measuredHeight,
-                        getCurrentCornerRadius()
-                    )
+                    outline.setRoundRect(measuredBounds, getCurrentCornerRadius())
                 }
             }
     }
 
     override fun onDraw(canvas: Canvas) {
-        when (uiState) {
-            is Uninitialized -> {}
+        when (val uiStateVal = uiState) {
+            is Uninitialized -> drawBackgroundOnly(canvas, Color.BLACK)
             is LiveTile -> drawTransparentUiState(canvas)
+            is Snapshot -> drawSnapshotState(canvas, uiStateVal)
+            is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor)
         }
     }
 
+    private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) {
+        backgroundPaint.color = backgroundColor
+        canvas.drawRect(measuredBounds, backgroundPaint)
+    }
+
     override fun onConfigurationChanged(newConfig: Configuration?) {
         super.onConfigurationChanged(newConfig)
 
@@ -112,7 +127,12 @@
     }
 
     private fun drawTransparentUiState(canvas: Canvas) {
-        canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), CLEAR_PAINT)
+        canvas.drawRect(measuredBounds, CLEAR_PAINT)
+    }
+
+    private fun drawSnapshotState(canvas: Canvas, snapshot: Snapshot) {
+        drawBackgroundOnly(canvas, snapshot.backgroundColor)
+        canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null)
     }
 
     private fun getCurrentCornerRadius() =
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
index 71bc865..4511ea7 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
@@ -16,32 +16,76 @@
 
 package com.android.quickstep.task.thumbnail
 
+import android.annotation.ColorInt
+import android.graphics.Rect
+import androidx.core.graphics.ColorUtils
+import com.android.quickstep.recents.data.RecentTasksRepository
 import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
-class TaskThumbnailViewModel(recentsViewData: RecentsViewData, taskViewData: TaskViewData) {
-    private val task = MutableStateFlow<TaskThumbnail?>(null)
+@OptIn(ExperimentalCoroutinesApi::class)
+class TaskThumbnailViewModel(
+    recentsViewData: RecentsViewData,
+    taskViewData: TaskViewData,
+    private val tasksRepository: RecentTasksRepository,
+) {
+    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
+    private var boundTaskIsRunning = false
 
     val recentsFullscreenProgress = recentsViewData.fullscreenProgress
     val inheritedScale =
         combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
             recentsScale * taskScale
         }
-    val uiState =
-        task.map { taskVal ->
-            when {
-                taskVal == null -> Uninitialized
-                taskVal.isRunning -> LiveTile
-                else -> Uninitialized
+    val uiState: Flow<TaskThumbnailUiState> =
+        task
+            .flatMapLatest { taskFlow ->
+                taskFlow.map { taskVal ->
+                    when {
+                        taskVal == null -> Uninitialized
+                        boundTaskIsRunning -> LiveTile
+                        isBackgroundOnly(taskVal) ->
+                            BackgroundOnly(taskVal.colorBackground.removeAlpha())
+                        isSnapshotState(taskVal) -> {
+                            val bitmap = taskVal.thumbnail?.thumbnail!!
+                            Snapshot(
+                                bitmap,
+                                Rect(0, 0, bitmap.width, bitmap.height),
+                                taskVal.colorBackground.removeAlpha()
+                            )
+                        }
+                        else -> Uninitialized
+                    }
+                }
             }
-        }
+            .distinctUntilChanged()
 
-    fun bind(task: TaskThumbnail) {
-        this.task.value = task
+    fun bind(taskThumbnail: TaskThumbnail) {
+        boundTaskIsRunning = taskThumbnail.isRunning
+        task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
     }
+
+    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
+
+    private fun isSnapshotState(task: Task): Boolean {
+        val thumbnailPresent = task.thumbnail?.thumbnail != null
+        val taskLocked = task.isLocked
+
+        return thumbnailPresent && !taskLocked
+    }
+
+    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
 }
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index e4e2eb2..6f9cbfd 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -27,6 +27,8 @@
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_NONE;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition;
@@ -186,6 +188,10 @@
         }
 
         @PersistentSnapPosition int snapPosition = gtv.getSnapPosition();
+        if (snapPosition == SNAP_TO_NONE) {
+            // Free snap mode is enabled, just save it as 50/50 split.
+            snapPosition = SNAP_TO_50_50;
+        }
         if (!isPersistentSnapPosition(snapPosition)) {
             // If we received an illegal snap position, log an error and do not create the app pair
             Log.wtf(TAG, "Tried to save an app pair with illegal snapPosition "
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index a8ebe51..6377bf4 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -23,6 +23,7 @@
 
 import android.app.ActivityOptions;
 import android.content.ActivityNotFoundException;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.LauncherApps.AppUsageLimit;
@@ -38,6 +39,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
@@ -49,6 +51,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
+import com.android.quickstep.TaskUtils;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.systemui.shared.recents.model.Task;
 
@@ -68,13 +71,15 @@
     private static final int SPLIT_GRID_BANNER_LARGE = 1;
     /** Used for grid task view, only showing icon */
     private static final int SPLIT_GRID_BANNER_SMALL = 2;
+
     @IntDef(value = {
             SPLIT_BANNER_FULLSCREEN,
             SPLIT_GRID_BANNER_LARGE,
             SPLIT_GRID_BANNER_SMALL,
     })
     @Retention(RetentionPolicy.SOURCE)
-    @interface SPLIT_BANNER_CONFIG{}
+    @interface SplitBannerConfig {
+    }
 
     static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
     static final int MINUTE_MS = 60000;
@@ -88,7 +93,6 @@
     private Task mTask;
     private boolean mHasLimit;
 
-    private long mAppUsageLimitTimeMs;
     private long mAppRemainingTimeMs;
     @Nullable
     private View mBanner;
@@ -96,10 +100,11 @@
     private float mBannerOffsetPercentage;
     @Nullable
     private SplitBounds mSplitBounds;
-    private int mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN;
     private float mSplitOffsetTranslationY;
     private float mSplitOffsetTranslationX;
 
+    private boolean mIsDestroyed = false;
+
     public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) {
         mContainer = container;
         mTaskView = taskView;
@@ -110,12 +115,10 @@
         mHasLimit = false;
         mTaskView.setContentDescription(mTask.titleDescription);
         replaceBanner(null);
-        mAppUsageLimitTimeMs = -1;
         mAppRemainingTimeMs = -1;
     }
 
     private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
-        mAppUsageLimitTimeMs = appUsageLimitTimeMs;
         mAppRemainingTimeMs = appRemainingTimeMs;
         mHasLimit = true;
         TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast,
@@ -138,89 +141,95 @@
     }
 
     public void initialize(Task task) {
-        mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1;
+        if (mIsDestroyed) {
+            throw new IllegalStateException("Cannot re-initialize a destroyed toast");
+        }
         mTask = task;
         ORDERED_BG_EXECUTOR.execute(() -> {
-                    AppUsageLimit usageLimit = null;
-                    try {
-                        usageLimit = mLauncherApps.getAppUsageLimit(
-                                mTask.getTopComponent().getPackageName(),
-                                UserHandle.of(mTask.key.userId));
-                    } catch (Exception e) {
-                        Log.e(TAG, "Error initializing digital well being toast", e);
-                    }
-                    final long appUsageLimitTimeMs =
-                            usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
-                    final long appRemainingTimeMs =
-                            usageLimit != null ? usageLimit.getUsageRemaining() : -1;
+            AppUsageLimit usageLimit = null;
+            try {
+                usageLimit = mLauncherApps.getAppUsageLimit(
+                        mTask.getTopComponent().getPackageName(),
+                        UserHandle.of(mTask.key.userId));
+            } catch (Exception e) {
+                Log.e(TAG, "Error initializing digital well being toast", e);
+            }
+            final long appUsageLimitTimeMs =
+                    usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
+            final long appRemainingTimeMs =
+                    usageLimit != null ? usageLimit.getUsageRemaining() : -1;
 
-                    mTaskView.post(() -> {
-                        if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
-                            setNoLimit();
-                        } else {
-                            setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
-                        }
-                    });
-
+            mTaskView.post(() -> {
+                if (mIsDestroyed) {
+                    return;
                 }
-        );
+                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+                    setNoLimit();
+                } else {
+                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
+                }
+            });
+        });
     }
 
-    public void setSplitConfiguration(SplitBounds splitBounds) {
+    /**
+     * Mark the DWB toast as destroyed and remove banner from TaskView.
+     */
+    public void destroy() {
+        mIsDestroyed = true;
+        mTaskView.post(() -> replaceBanner(null));
+    }
+
+    public void setSplitBounds(@Nullable SplitBounds splitBounds) {
         mSplitBounds = splitBounds;
+    }
+
+    private @SplitBannerConfig int getSplitBannerConfig() {
         if (mSplitBounds == null
                 || !mContainer.getDeviceProfile().isTablet
                 || mTaskView.isFocusedTask()) {
-            mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN;
-            return;
+            return SPLIT_BANNER_FULLSCREEN;
         }
 
         // For portrait grid only height of task changes, not width. So we keep the text the same
         if (!mContainer.getDeviceProfile().isLeftRightSplit) {
-            mSplitBannerConfig = SPLIT_GRID_BANNER_LARGE;
-            return;
+            return SPLIT_GRID_BANNER_LARGE;
         }
 
         // For landscape grid, for 30% width we only show icon, otherwise show icon and time
         if (mTask.key.id == mSplitBounds.leftTopTaskId) {
-            mSplitBannerConfig = mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ?
-                    SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
+            return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY
+                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
         } else {
-            mSplitBannerConfig = mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ?
-                    SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
+            return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY
+                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
         }
     }
 
     private String getReadableDuration(
             Duration duration,
-            FormatWidth formatWidthHourAndMinute,
-            @StringRes int durationLessThanOneMinuteStringId,
-            boolean forceFormatWidth) {
+            @StringRes int durationLessThanOneMinuteStringId) {
         int hours = Math.toIntExact(duration.toHours());
         int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
 
-        // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero.
+        // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
         if (hours > 0 && minutes > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute)
+            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW)
                     .formatMeasures(
                             new Measure(hours, MeasureUnit.HOUR),
                             new Measure(minutes, MeasureUnit.MINUTE));
         }
 
-        // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced).
+        // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
         if (hours > 0) {
-            return MeasureFormat.getInstance(
-                    Locale.getDefault(),
-                    forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
-                    .formatMeasures(new Measure(hours, MeasureUnit.HOUR));
+            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
+                    new Measure(hours, MeasureUnit.HOUR));
         }
 
-        // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced).
+        // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
         if (minutes > 0) {
-            return MeasureFormat.getInstance(
-                    Locale.getDefault()
-                    , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
-                    .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE));
+            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
+                    new Measure(minutes, MeasureUnit.MINUTE));
         }
 
         // Use a specific string for usage less than one minute but non-zero.
@@ -229,13 +238,12 @@
         }
 
         // Otherwise, return 0-minute string.
-        return MeasureFormat.getInstance(
-                Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
-                .formatMeasures(new Measure(0, MeasureUnit.MINUTE));
+        return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
+                new Measure(0, MeasureUnit.MINUTE));
     }
 
     /**
-     * Returns text to show for the banner depending on {@link #mSplitBannerConfig}
+     * Returns text to show for the banner depending on {@link #getSplitBannerConfig()}
      * If {@param forContentDesc} is {@code true}, this will always return the full
      * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN}
      */
@@ -245,16 +253,16 @@
                         (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
                         remainingTime);
         String readableDuration = getReadableDuration(duration,
-                FormatWidth.NARROW,
-                R.string.shorter_duration_less_than_one_minute,
-                false /* forceFormatWidth */);
-        if (forContentDesc || mSplitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
+                R.string.shorter_duration_less_than_one_minute
+                /* forceFormatWidth */);
+        @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig();
+        if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
             return mContainer.asContext().getString(
                     R.string.time_left_for_app,
                     readableDuration);
         }
 
-        if (mSplitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
+        if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
             // show no text
             return "";
         } else { // SPLIT_GRID_BANNER_LARGE
@@ -309,7 +317,7 @@
 
     private void setBanner(@Nullable View view) {
         mBanner = view;
-        if (view != null && mTaskView.getRecentsView() != null) {
+        if (mBanner != null && mTaskView.getRecentsView() != null) {
             setupAndAddBanner();
             setBannerOutline();
         }
@@ -395,4 +403,36 @@
 
         mBanner.setVisibility(visibility);
     }
+
+    private int getAccessibilityActionId() {
+        return (mSplitBounds != null
+                && mSplitBounds.rightBottomTaskId == mTask.key.id)
+                ? R.id.action_digital_wellbeing_bottom_right
+                : R.id.action_digital_wellbeing_top_left;
+    }
+
+    @Nullable
+    public AccessibilityNodeInfo.AccessibilityAction getDWBAccessibilityAction() {
+        if (!hasLimit()) {
+            return null;
+        }
+
+        Context context = mContainer.asContext();
+        String label =
+                (mTaskView.containsMultipleTasks())
+                        ? context.getString(
+                        R.string.split_app_usage_settings,
+                        TaskUtils.getTitle(context, mTask)
+                ) : context.getString(R.string.accessibility_app_usage_settings);
+        return new AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label);
+    }
+
+    public boolean handleAccessibilityAction(int action) {
+        if (getAccessibilityActionId() == action) {
+            openAppUsageSettings(mTaskView);
+            return true;
+        } else {
+            return false;
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index efbfa09..d6a3376 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -162,6 +162,7 @@
                         PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT
                     )
             }
+        taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) }
         setOrientationState(orientedState)
     }
 
@@ -240,6 +241,10 @@
 
     fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) {
         splitBoundsConfig = splitBounds
+        taskContainers.forEach {
+            it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig)
+            it.digitalWellBeingToast?.initialize(it.task)
+        }
         invalidate()
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 83a2ceb..604d072 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -20,6 +20,7 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
@@ -49,6 +50,7 @@
  */
 public class OverviewActionsView<T extends OverlayUICallbacks> extends FrameLayout
         implements OnClickListener, Insettable {
+    public static final String TAG = "OverviewActionsView";
     private final Rect mInsets = new Rect();
 
     @IntDef(flag = true, value = {
@@ -110,9 +112,11 @@
 
     /** Container for the action buttons below a focused, non-split Overview tile. */
     protected LinearLayout mActionButtons;
-    /** Container for the action buttons below a focused, split Overview tile. */
-    protected LinearLayout mGroupActionButtons;
     private Button mSplitButton;
+    /**
+     * The "save app pair" button. Currently this is the only button that is not contained in
+     * mActionButtons, since it is the sole button that appears for a grouped task.
+     */
     private Button mSaveAppPairButton;
 
     @ActionsHiddenFlags
@@ -150,15 +154,16 @@
         super.onFinishInflate();
         // Initialize 2 view containers: one for single tasks, one for grouped tasks.
         // These will take up the same space on the screen and alternate visibility as needed.
+        // Currently, the only grouped task action is "save app pairs".
         mActionButtons = findViewById(R.id.action_buttons);
-        mGroupActionButtons = findViewById(R.id.group_action_buttons);
-        // Initialize a list to hold alphas for mActionButtons and mGroupActionButtons.
+        mSaveAppPairButton = findViewById(R.id.action_save_app_pair);
+        // Initialize a list to hold alphas for mActionButtons and any group action buttons.
         mMultiValueAlphas[ACTIONS_ALPHAS] = new MultiValueAlpha(mActionButtons, NUM_ALPHAS);
         mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] =
-                new MultiValueAlpha(mGroupActionButtons, NUM_ALPHAS);
+                new MultiValueAlpha(mSaveAppPairButton, NUM_ALPHAS);
         Arrays.stream(mMultiValueAlphas).forEach(a -> a.setUpdateVisibility(true));
-        // To control alpha simultaneously on mActionButtons and mGroupActionButtons, we set up an
-        // AnimatedFloat for each alpha property.
+        // To control alpha simultaneously on mActionButtons and any group action buttons, we set up
+        // an AnimatedFloat for each alpha property.
         for (int i = 0; i < NUM_ALPHAS; i++) {
             final int index = i;
             mAlphaProperties[index] = new AnimatedFloat(() -> {
@@ -175,7 +180,6 @@
         screenshotButton.setOnClickListener(this);
         mSplitButton = findViewById(R.id.action_split);
         mSplitButton.setOnClickListener(this);
-        mSaveAppPairButton = findViewById(R.id.action_save_app_pair);
         mSaveAppPairButton.setOnClickListener(this);
     }
 
@@ -252,6 +256,8 @@
      *                      pair.
      */
     public void updateForGroupedTask(boolean isGroupedTask, boolean canSaveAppPair) {
+        Log.d(TAG, "updateForGroupedTask() called with: isGroupedTask = [" + isGroupedTask
+                + "], canSaveAppPair = [" + canSaveAppPair + "]");
         mIsGroupedTask = isGroupedTask;
         mCanSaveAppPair = canSaveAppPair;
         updateActionButtonsVisibility();
@@ -271,6 +277,8 @@
         assert mDp != null;
         boolean showSingleTaskActions = !mIsGroupedTask;
         boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair;
+        Log.d(TAG, "updateActionButtonsVisibility() called: showSingleTaskActions = ["
+                + showSingleTaskActions + ", showGroupActions = [" + showGroupActions + "]");
         getActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showSingleTaskActions ? 1 : 0);
         getGroupActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showGroupActions ? 1 : 0);
     }
@@ -336,7 +344,7 @@
      */
     public boolean areActionsButtonsVisible() {
         return mActionButtons.getVisibility() == View.VISIBLE
-                || mGroupActionButtons.getVisibility() == View.VISIBLE;
+                || mSaveAppPairButton.getVisibility() == View.VISIBLE;
     }
 
     /**
@@ -350,11 +358,11 @@
     /** Updates vertical margins for different navigation mode or configuration changes. */
     public void updateVerticalMargin(NavigationMode mode) {
         updateActionBarPosition(mActionButtons);
-        updateActionBarPosition(mGroupActionButtons);
+        updateActionBarPosition(mSaveAppPairButton);
     }
 
     /** Positions actions buttons according to device settings and insets. */
-    private void updateActionBarPosition(LinearLayout actionBar) {
+    private void updateActionBarPosition(View actionBar) {
         if (mDp == null) {
             return;
         }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5eee64d..9dfcd08 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -188,6 +188,7 @@
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
+import com.android.quickstep.recents.data.TasksRepository;
 import com.android.quickstep.recents.viewmodel.RecentsViewData;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
@@ -216,6 +217,7 @@
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
 import com.android.wm.shell.shared.DesktopModeStatus;
 
@@ -305,6 +307,7 @@
     public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f;
     public static final VibrationEffect SCROLL_VIBRATION_FALLBACK =
             VibrationConstants.EFFECT_TEXTURE_TICK;
+    public static final int UNBOUND_TASK_VIEW_ID = -1;
 
     /**
      * Can be used to tint the color of the RecentsView to simulate a scrim that can views
@@ -456,6 +459,7 @@
     private static final float FOREGROUND_SCRIM_TINT = 0.32f;
 
     public final RecentsViewData mRecentsViewData = new RecentsViewData();
+    public final TasksRepository mTasksRepository;
 
     protected final RecentsOrientedState mOrientationState;
     protected final BaseContainerInterface<STATE_TYPE, CONTAINER_TYPE> mSizeStrategy;
@@ -800,6 +804,12 @@
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
+        if (enableRefactorTaskThumbnail()) {
+            mTasksRepository = new TasksRepository(
+                    mModel, mModel.getThumbnailCache(), mModel.getIconCache());
+        } else {
+            mTasksRepository = null;
+        }
 
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
@@ -1165,7 +1175,6 @@
             } else {
                 mTaskViewPool.recycle(taskView);
             }
-            taskView.setTaskViewId(-1);
             mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
         }
     }
@@ -2362,6 +2371,8 @@
             upper = Math.min(centerPageIndex + 2, numChildren - 1);
         }
 
+        List<Integer> visibleTaskIds = new ArrayList<>();
+
         // Update the task data for the in/visible children
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
@@ -2381,6 +2392,10 @@
                 List<Task> tasksToUpdate = containers.stream()
                         .map(TaskContainer::getTask)
                         .collect(Collectors.toCollection(ArrayList::new));
+                if (enableRefactorTaskThumbnail()) {
+                    visibleTaskIds.addAll(
+                            tasksToUpdate.stream().map((task) -> task.key.id).toList());
+                }
                 if (mTmpRunningTasks != null) {
                     for (Task t : mTmpRunningTasks) {
                         // Skip loading if this is the task that we are animating into
@@ -2416,6 +2431,9 @@
                 }
             }
         }
+        if (enableRefactorTaskThumbnail()) {
+            mTasksRepository.setVisibleTasks(visibleTaskIds);
+        }
     }
 
     /**
@@ -2602,6 +2620,9 @@
         if (!mModel.isTaskListValid(mTaskListChangeId)) {
             mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
                     .getFilter(mFilterState.getPackageNameToFilter()));
+            if (enableRefactorTaskThumbnail()) {
+                mTasksRepository.getAllTaskData(/* forceRefresh = */ true);
+            }
         }
     }
 
@@ -6284,20 +6305,22 @@
      * Moves the provided task into desktop mode, and invoke {@code successCallback} if succeeded.
      */
     public void moveTaskToDesktop(TaskContainer taskContainer,
+            DesktopModeTransitionSource transitionSource,
             Runnable successCallback) {
         if (!DesktopModeStatus.canEnterDesktopMode(mContext)) {
             return;
         }
         switchToScreenshot(() -> finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false,
-                () -> moveTaskToDesktopInternal(taskContainer, successCallback)));
+                () -> moveTaskToDesktopInternal(taskContainer, successCallback, transitionSource)));
     }
 
     private void moveTaskToDesktopInternal(TaskContainer taskContainer,
-            Runnable successCallback) {
+            Runnable successCallback, DesktopModeTransitionSource transitionSource) {
         if (mDesktopRecentsTransitionController == null) {
             return;
         }
-        mDesktopRecentsTransitionController.moveToDesktop(taskContainer.getTask().key.id);
+        mDesktopRecentsTransitionController.moveToDesktop(taskContainer.getTask().key.id,
+                transitionSource);
         successCallback.run();
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 05b9d40..cae23a1 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -38,6 +38,7 @@
 import android.view.ViewGroup
 import android.view.ViewStub
 import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
 import android.widget.FrameLayout
 import android.widget.Toast
 import androidx.annotation.IntDef
@@ -67,7 +68,6 @@
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SafeCloseable
 import com.android.launcher3.util.SplitConfigurationOptions
-import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
@@ -93,6 +93,7 @@
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.quickstep.util.TaskCornerRadius
 import com.android.quickstep.util.TaskRemovedDuringLaunchListener
+import com.android.quickstep.views.RecentsView.UNBOUND_TASK_VIEW_ID
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.system.ActivityManagerWrapper
@@ -132,19 +133,26 @@
     val taskIds: IntArray
         /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
         get() = taskContainers.map { it.task.key.id }.toIntArray()
+
     val thumbnailViews: Array<TaskThumbnailViewDeprecated>
         get() = taskContainers.map { it.thumbnailViewDeprecated }.toTypedArray()
+
     val isGridTask: Boolean
         /** Returns whether the task is part of overview grid and not being focused. */
         get() = container.deviceProfile.isTablet && !isFocusedTask
+
     val isRunningTask: Boolean
         get() = this === recentsView?.runningTaskView
+
     val isFocusedTask: Boolean
         get() = this === recentsView?.focusedTaskView
+
     val taskCornerRadius: Float
         get() = currentFullscreenParams.cornerRadius
+
     val recentsView: RecentsView<*, *>?
         get() = parent as? RecentsView<*, *>
+
     val pagedOrientationHandler: RecentsPagedOrientationHandler
         get() = orientedState.orientationHandler
 
@@ -152,10 +160,12 @@
     val firstTask: Task
         /** Returns the first task bound to this TaskView. */
         get() = taskContainers[0].task
+
     @get:Deprecated("Use [taskContainers] instead.")
     val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated
         /** Returns the first thumbnailView of the TaskView. */
         get() = taskContainers[0].thumbnailViewDeprecated
+
     @get:Deprecated("Use [taskContainers] instead.")
     val firstItemInfo: ItemInfo
         get() = taskContainers[0].itemInfo
@@ -172,6 +182,7 @@
          * not change according to a temporary state.
          */
         get() = Utilities.mapRange(gridProgress, nonGridScale, 1f)
+
     protected val persistentTranslationX: Float
         /**
          * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does
@@ -181,42 +192,50 @@
             (getNonGridTrans(nonGridTranslationX) +
                 getGridTrans(this.gridTranslationX) +
                 getNonGridTrans(nonGridPivotTranslationX))
+
     protected val persistentTranslationY: Float
         /**
          * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does
          * not change according to a temporary state (e.g. task offset).
          */
         get() = boxTranslationY + getGridTrans(gridTranslationY)
+
     protected val primarySplitTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getPrimaryValue(
                 SPLIT_SELECT_TRANSLATION_X,
                 SPLIT_SELECT_TRANSLATION_Y
             )
+
     protected val secondarySplitTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(
                 SPLIT_SELECT_TRANSLATION_X,
                 SPLIT_SELECT_TRANSLATION_Y
             )
+
     protected val primaryDismissTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)
+
     protected val secondaryDismissTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)
+
     protected val primaryTaskOffsetTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getPrimaryValue(
                 TASK_OFFSET_TRANSLATION_X,
                 TASK_OFFSET_TRANSLATION_Y
             )
+
     protected val secondaryTaskOffsetTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(
                 TASK_OFFSET_TRANSLATION_X,
                 TASK_OFFSET_TRANSLATION_Y
             )
+
     protected val taskResistanceTranslationProperty: FloatProperty<TaskView>
         get() =
             pagedOrientationHandler.getSecondaryValue(
@@ -233,9 +252,10 @@
     /** Returns a list of all TaskContainers in the TaskView. */
     lateinit var taskContainers: List<TaskContainer>
         protected set
+
     lateinit var orientedState: RecentsOrientedState
 
-    var taskViewId = -1
+    var taskViewId = UNBOUND_TASK_VIEW_ID
     var isEndQuickSwitchCuj = false
 
     // Various animation progress variables.
@@ -263,46 +283,55 @@
             field = value
             onModalnessUpdated(field)
         }
+
     protected var taskThumbnailSplashAlpha = 0f
         set(value) {
             field = value
             applyThumbnailSplashAlpha()
         }
+
     protected var nonGridScale = 1f
         set(value) {
             field = value
             applyScale()
         }
+
     private var dismissScale = 1f
         set(value) {
             field = value
             applyScale()
         }
+
     private var dismissTranslationX = 0f
         set(value) {
             field = value
             applyTranslationX()
         }
+
     private var dismissTranslationY = 0f
         set(value) {
             field = value
             applyTranslationY()
         }
+
     private var taskOffsetTranslationX = 0f
         set(value) {
             field = value
             applyTranslationX()
         }
+
     private var taskOffsetTranslationY = 0f
         set(value) {
             field = value
             applyTranslationY()
         }
+
     private var taskResistanceTranslationX = 0f
         set(value) {
             field = value
             applyTranslationX()
         }
+
     private var taskResistanceTranslationY = 0f
         set(value) {
             field = value
@@ -320,6 +349,7 @@
             field = value
             applyTranslationX()
         }
+
     var gridTranslationY = 0f
         protected set(value) {
             field = value
@@ -338,6 +368,7 @@
             field = value
             applyTranslationX()
         }
+
     protected var nonGridPivotTranslationX = 0f
         set(value) {
             field = value
@@ -349,16 +380,19 @@
             field = value
             applyTranslationY()
         }
+
     private var splitSelectTranslationX = 0f
         set(value) {
             field = value
             applyTranslationX()
         }
+
     protected var stableAlpha = 1f
         set(value) {
             field = value
             alpha = stableAlpha
         }
+
     protected var shouldShowScreenshot = false
         get() = !isRunningTask || field
     /** Enable or disable showing border on hover and focus change */
@@ -374,6 +408,7 @@
             hoverBorderAnimator?.setBorderVisibility(visible = field && isHovered, animated = true)
             focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true)
         }
+
     protected var iconScaleAnimStartProgress = 0f
     private var focusTransitionProgress = 1f
 
@@ -502,7 +537,6 @@
         resetPersistentViewTransforms()
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
-        // TODO(b/334825222): Implement thumbnail/snapshot for the new [TaskThumbnailView].
         if (enableRefactorTaskThumbnail()) {
             notifyIsRunningTaskUpdated()
         } else {
@@ -511,6 +545,8 @@
         setOverlayEnabled(false)
         onTaskListVisibilityChanged(false)
         borderEnabled = false
+        taskViewId = UNBOUND_TASK_VIEW_ID
+        taskContainers.forEach { it.destroy() }
     }
 
     // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
@@ -520,11 +556,12 @@
         super.onInitializeAccessibilityNodeInfo(info)
         with(info) {
             addAction(
-                AccessibilityNodeInfo.AccessibilityAction(
-                    R.string.accessibility_close,
+                AccessibilityAction(
+                    R.id.action_close,
                     context.getText(R.string.accessibility_close)
                 )
             )
+
             taskContainers.forEach {
                 TraceHelper.allowIpcs("TV.a11yInfo") {
                     TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut ->
@@ -532,15 +569,12 @@
                     }
                 }
             }
-            // TODO(b/341672022): handle multiple digitalWellBeingToast accessibility actions
-            if (taskContainers[0].digitalWellBeingToast?.hasLimit() == true) {
-                addAction(
-                    AccessibilityNodeInfo.AccessibilityAction(
-                        R.string.accessibility_app_usage_settings,
-                        context.getText(R.string.accessibility_app_usage_settings)
-                    )
-                )
+
+            // Add DWB accessibility action at the end of the list
+            taskContainers.forEach {
+                it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction)
             }
+
             recentsView?.let {
                 collectionItemInfo =
                     AccessibilityNodeInfo.CollectionItemInfo.obtain(
@@ -555,16 +589,17 @@
     }
 
     override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
-        if (action == R.string.accessibility_close) {
+        // TODO(b/343708271): Add support for multiple tasks per action.
+        if (action == R.id.action_close) {
             recentsView?.dismissTask(this, true /*animateTaskView*/, true /*removeTask*/)
             return true
         }
-        if (action == R.string.accessibility_app_usage_settings) {
-            // TODO(b/341672022): handle multiple digitalWellBeingToast accessibility actions
-            taskContainers[0].digitalWellBeingToast?.openAppUsageSettings(this)
-            return true
-        }
+
         taskContainers.forEach {
+            if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) {
+                return true
+            }
+
             TaskOverlayFactory.getEnabledShortcuts(this, it).forEach { shortcut ->
                 if (shortcut.hasHandlerForAction(action)) {
                     shortcut.onClick(this)
@@ -572,6 +607,7 @@
                 }
             }
         }
+
         return super.performAccessibilityAction(action, arguments)
     }
 
@@ -780,22 +816,19 @@
         val recentsModel = RecentsModel.INSTANCE.get(context)
         // These calls are no-ops if the data is already loaded, try and load the high
         // resolution thumbnail if the state permits
-        if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
-            if (!enableRefactorTaskThumbnail()) {
-                // TODO(b/334825222) add thumbnail state
-                taskContainers.forEach {
-                    if (visible) {
-                        recentsModel.thumbnailCache
-                            .updateThumbnailInBackground(it.task) { thumbnailData ->
-                                it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
-                            }
-                            ?.also { request -> pendingThumbnailLoadRequests.add(request) }
-                    } else {
-                        it.thumbnailViewDeprecated.setThumbnail(null, null)
-                        // Reset the task thumbnail reference as well (it will be fetched from the
-                        // cache or reloaded next time we need it)
-                        it.task.thumbnail = null
-                    }
+        if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL) && !enableRefactorTaskThumbnail()) {
+            taskContainers.forEach {
+                if (visible) {
+                    recentsModel.thumbnailCache
+                        .updateThumbnailInBackground(it.task) { thumbnailData ->
+                            it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
+                        }
+                        ?.also { request -> pendingThumbnailLoadRequests.add(request) }
+                } else {
+                    it.thumbnailViewDeprecated.setThumbnail(null, null)
+                    // Reset the task thumbnail reference as well (it will be fetched from the
+                    // cache or reloaded next time we need it)
+                    it.task.thumbnail = null
                 }
             }
         }
@@ -803,12 +836,12 @@
             taskContainers.forEach {
                 if (visible) {
                     recentsModel.iconCache
-                        .updateIconInBackground(it.task) { thumbnailData ->
-                            setIcon(it.iconView, thumbnailData.icon)
+                        .updateIconInBackground(it.task) { task ->
+                            setIcon(it.iconView, task.icon)
                             if (enableOverviewIconMenu()) {
-                                setText(it.iconView, thumbnailData.title)
+                                setText(it.iconView, task.title)
                             }
-                            it.digitalWellBeingToast?.initialize(thumbnailData)
+                            it.digitalWellBeingToast?.initialize(task)
                         }
                         ?.also { request -> pendingIconLoadRequests.add(request) }
                 } else {
@@ -861,7 +894,7 @@
 
     open fun refreshThumbnails(thumbnailDatas: HashMap<Int, ThumbnailData?>?) {
         if (enableRefactorTaskThumbnail()) {
-            // TODO(b/334825222) add thumbnail logic
+            // TODO(b/342560598) add thumbnail logic
             return
         }
 
@@ -1556,11 +1589,6 @@
     ) {
         val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
 
-        @IdRes
-        val a11yNodeId: Int =
-            if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) R.id.split_bottomRight_appInfo
-            else R.id.split_topLeft_appInfo
-
         val snapshotView: View
             get() = thumbnailView ?: thumbnailViewDeprecated
 
@@ -1588,10 +1616,17 @@
         val taskView: TaskView
             get() = this@TaskView
 
+        fun destroy() {
+            digitalWellBeingToast?.destroy()
+            thumbnailView?.let { taskView.removeView(it) }
+        }
+
         // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
         //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
         fun bindThumbnailView() {
-            thumbnailView?.viewModel?.bind(TaskThumbnail(task, isRunningTask))
+            // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
+            //  this should be decided inside TaskThumbnailViewModel.
+            thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, isRunningTask))
         }
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
new file mode 100644
index 0000000..e160627
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.quickstep.recents.data
+
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+
+class FakeTasksRepository : RecentTasksRepository {
+    private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
+    private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
+    private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
+
+    override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks
+
+    override fun getTaskDataById(taskId: Int): Flow<Task?> =
+        getAllTaskData().map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+
+    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+        visibleTasks.value = visibleTaskIdList
+        tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } }
+    }
+
+    fun seedTasks(tasks: List<Task>) {
+        this.tasks.value = tasks
+    }
+
+    fun seedThumbnailData(thumbnailDataMap: Map<Int, ThumbnailData>) {
+        this.thumbnailDataMap = thumbnailDataMap
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index efd7bec..3b8754c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -16,33 +16,51 @@
 
 package com.android.quickstep.task.thumbnail
 
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Rect
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeTasksRepository
 import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskViewData
 import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 class TaskThumbnailViewModelTest {
     private val recentsViewData = RecentsViewData()
     private val taskViewData = TaskViewData()
-    private val systemUnderTest = TaskThumbnailViewModel(recentsViewData, taskViewData)
+    private val tasksRepository = FakeTasksRepository()
+    private val systemUnderTest =
+        TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository)
+
+    private val tasks = (0..5).map(::createTaskWithId)
 
     @Test
     fun initialStateIsUninitialized() = runTest {
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.Uninitialized)
+        assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
     }
 
     @Test
     fun bindRunningTask_thenStateIs_LiveTile() = runTest {
-        val taskThumbnail = TaskThumbnail(Task(), isRunning = true)
+        tasksRepository.seedTasks(tasks)
+        val taskThumbnail = TaskThumbnail(taskId = 1, isRunning = true)
         systemUnderTest.bind(taskThumbnail)
 
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.LiveTile)
+        assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
     }
 
     @Test
@@ -65,15 +83,96 @@
     }
 
     @Test
-    fun bindRunningTaskThenStoppedTask_thenStateIs_Uninitialized() = runTest {
-        // TODO(b/334825222): Change the expectation here when snapshot state is implemented
-        val task = Task()
-        val runningTask = TaskThumbnail(task, isRunning = true)
-        val stoppedTask = TaskThumbnail(task, isRunning = false)
-        systemUnderTest.bind(runningTask)
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.LiveTile)
+    fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
+        runTest {
+            tasksRepository.seedTasks(tasks)
+            val runningTask = TaskThumbnail(taskId = 1, isRunning = true)
+            val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
+            systemUnderTest.bind(runningTask)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+
+            systemUnderTest.bind(stoppedTask)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
+        tasksRepository.seedTasks(tasks)
+        val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false)
 
         systemUnderTest.bind(stoppedTask)
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.Uninitialized)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+    }
+
+    @Test
+    fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest {
+        tasksRepository.seedThumbnailData(mapOf(2 to createThumbnailData()))
+        tasks[2].isLocked = true
+        tasksRepository.seedTasks(tasks)
+        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+    }
+
+    @Test
+    fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest {
+        val expectedThumbnailData = createThumbnailData()
+        tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+        tasksRepository.seedTasks(tasks)
+        tasksRepository.setVisibleTasks(listOf(2))
+        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(
+                Snapshot(
+                    backgroundColor = Color.rgb(2, 2, 2),
+                    bitmap = expectedThumbnailData.thumbnail!!,
+                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
+                )
+            )
+    }
+
+    @Test
+    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest {
+        val expectedThumbnailData = createThumbnailData()
+        tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData))
+        tasksRepository.seedTasks(tasks)
+        val recentTask = TaskThumbnail(taskId = 2, isRunning = false)
+
+        systemUnderTest.bind(recentTask)
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        tasksRepository.setVisibleTasks(listOf(2))
+        assertThat(systemUnderTest.uiState.first())
+            .isEqualTo(
+                Snapshot(
+                    backgroundColor = Color.rgb(2, 2, 2),
+                    bitmap = expectedThumbnailData.thumbnail!!,
+                    drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
+                )
+            )
+    }
+
+    private fun createTaskWithId(taskId: Int) =
+        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+        }
+
+    private fun createThumbnailData(): ThumbnailData {
+        val bitmap = mock<Bitmap>()
+        whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
+        whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
+
+        return ThumbnailData(thumbnail = bitmap)
+    }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 36f2ccf..50b5df1 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.Task.TaskKey
 import com.android.window.flags.Flags
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.DesktopModeStatus
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
@@ -152,8 +153,8 @@
         whenever(launcher.statsLogManager).thenReturn(statsLogManager)
         whenever(statsLogManager.logger()).thenReturn(statsLogger)
         whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
-        whenever(recentsView.moveTaskToDesktop(any(), any())).thenAnswer {
-            val successCallback = it.getArgument<Runnable>(1)
+        whenever(recentsView.moveTaskToDesktop(any(), any(), any())).thenAnswer {
+            val successCallback = it.getArgument<Runnable>(2)
             successCallback.run()
         }
         doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo
@@ -169,7 +170,12 @@
         val allTypesExceptRebindSafe =
             AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
         verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe)
-        verify(recentsView).moveTaskToDesktop(eq(taskContainer), any())
+        verify(recentsView)
+            .moveTaskToDesktop(
+                eq(taskContainer),
+                eq(DesktopModeTransitionSource.APP_FROM_OVERVIEW),
+                any()
+            )
         verify(statsLogger).withItemInfo(workspaceItemInfo)
         verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP)
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index bfd7bdb..6be082a 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -129,8 +129,6 @@
                     overview.getCurrentTask()
                             .tapMenu()
                             .hasMenuItem("Save app pair"));
-        } else {
-            overview.getOverviewGroupActions().assertHasAction("Save app pair");
         }
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index e4f8b6c..2c23f86 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -37,6 +37,7 @@
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -47,8 +48,14 @@
     private static final String READ_DEVICE_CONFIG_PERMISSION =
             "android.permission.READ_DEVICE_CONFIG";
 
+    @Before
+    public void setup() {
+        mLauncher.injectFakeTrackpad();
+    }
+
     @After
     public void tearDown() {
+        mLauncher.ejectFakeTrackpad();
         mLauncher.setTrackpadGestureType(TrackpadGestureType.NONE);
     }
 
@@ -110,8 +117,8 @@
     }
 
     @Test
-    @NavigationModeSwitch
     @PortraitLandscape
+    @NavigationModeSwitch
     public void testQuickSwitchFromHome() throws Exception {
         assumeTrue(mLauncher.isTablet());
 
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
index cefe394..9c0f129 100644
--- a/res/layout/private_space_header.xml
+++ b/res/layout/private_space_header.xml
@@ -37,7 +37,7 @@
         android:gravity="center_vertical"
         android:layout_alignParentEnd="true"
         android:animateLayoutChanges="false">
-        <ImageButton
+        <com.android.launcher3.allapps.PrivateSpaceSettingsButton
             android:id="@+id/ps_settings_button"
             android:layout_width="@dimen/ps_header_image_height"
             android:layout_height="@dimen/ps_header_image_height"
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 0a381c8..f2ac2e8 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -124,7 +124,7 @@
     <string name="title_missing_notification_access" msgid="7503287056163941064">"सूचना के ऐक्सेस की ज़रूरत है"</string>
     <string name="msg_missing_notification_access" msgid="281113995110910548">"सूचना बिंदु दिखाने के लिए, <xliff:g id="NAME">%1$s</xliff:g> के ऐप्लिकेशन सूचना चालू करें"</string>
     <string name="title_change_settings" msgid="1376365968844349552">"सेटिंग बदलें"</string>
-    <string name="notification_dots_service_title" msgid="4284221181793592871">"नई सूचनाएं बताने वाला गोल निशान दिखाएं"</string>
+    <string name="notification_dots_service_title" msgid="4284221181793592871">"सूचनाएं बताने वाले डॉट दिखाएं"</string>
     <string name="developer_options_title" msgid="700788437593726194">"डेवलपर के लिए सेटिंग और टूल"</string>
     <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"होम स्क्रीन पर ऐप्लिकेशन के आइकॉन जोड़ें"</string>
     <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"नए ऐप्लिकेशन के लिए"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 96048eb..1844933 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -185,7 +185,7 @@
     <string name="remote_action_failed" msgid="1383965239183576790">"Не удалось выполнить действие (<xliff:g id="WHAT">%1$s</xliff:g>)."</string>
     <string name="private_space_label" msgid="2359721649407947001">"Частное пространство"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Нажмите, чтобы настроить или открыть"</string>
-    <string name="ps_container_title" msgid="4391796149519594205">"Доступно только вам"</string>
+    <string name="ps_container_title" msgid="4391796149519594205">"Частный профиль"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Настройки личного пространства"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Личное, разблокировано."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Личное, заблокировано."</string>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 2741158..aa83c01 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -320,6 +320,8 @@
     <dimen name="bg_popup_item_height">52dp</dimen>
     <dimen name="bg_popup_item_vertical_padding">12dp</dimen>
     <dimen name="pre_drag_view_scale">6dp</dimen>
+    <!-- Minimum size of the widget dragged view to keep it visible under the finger. -->
+    <dimen name="widget_drag_view_min_scale_down_size">70dp</dimen>
     <!-- an icon with shortcuts must be dragged this far before the container is removed. -->
     <dimen name="deep_shortcuts_start_drag_threshold">16dp</dimen>
     <!-- Possibly related to b/235886078, icon needs to be scaled up to match expected visual size of 32 dp -->
diff --git a/res/values/id.xml b/res/values/id.xml
index 7bb9396..28496b5 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -19,9 +19,6 @@
     <item type="id" name="view_type_widgets_space" />
     <item type="id" name="view_type_widgets_list" />
     <item type="id" name="view_type_widgets_header" />
-    <!--  Used for A11y actions in staged split to identify each task uniquely  -->
-    <item type="id" name="split_topLeft_appInfo" />
-    <item type="id" name="split_bottomRight_appInfo" />
 
     <!-- Accessibility actions -->
     <item type="id" name="action_remove" />
@@ -37,6 +34,12 @@
     <item type="id" name="action_remote_action_shortcut" />
     <item type="id" name="action_dismiss_prediction" />
     <item type="id" name="action_pin_prediction"/>
+    <item type="id" name="action_close"/>
+    <!--  Used for A11y actions in staged split to identify each task uniquely  -->
+    <item type="id" name="action_app_info_top_left" />
+    <item type="id" name="action_app_info_bottom_right" />
+    <item type="id" name="action_digital_wellbeing_top_left" />
+    <item type="id" name="action_digital_wellbeing_bottom_right" />
 
     <!-- QSB IDs. DO not change -->
     <item type="id" name="search_container_workspace" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ef0f0d8..207d246 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -41,6 +41,7 @@
     <!-- Title for an option to enter split screen mode for a given app -->
     <string name="recent_task_option_split_screen">Split screen</string>
     <string name="split_app_info_accessibility">App info for %1$s</string>
+    <string name="split_app_usage_settings">Usage settings for %1$s</string>
 
     <!-- App pairs -->
     <string name="save_app_pair">Save app pair</string>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b89d05e..2b30dc4 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -96,9 +96,7 @@
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
-import static com.android.launcher3.testing.shared.TestProtocol.CLOCK_ICON_DRAWABLE_LEAKING;
 import static com.android.launcher3.testing.shared.TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE;
-import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch;
 import static com.android.launcher3.util.SettingsCache.TOUCHPAD_NATURAL_SCROLLING;
@@ -163,6 +161,7 @@
 import androidx.annotation.StringRes;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.window.embedding.RuleController;
 
 import com.android.launcher3.DropTarget.DragObject;
@@ -423,7 +422,6 @@
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
-        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onCreate: instance=" + this);
         mStartupLatencyLogger = createStartupLatencyLogger(
                 sIsNewProcess
                         ? LockedUserState.get(this).isUserUnlockedAtLauncherStartup()
@@ -589,7 +587,8 @@
         setTitle(R.string.home_screen);
         mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
 
-        if (com.android.launcher3.Flags.enableTwoPaneLauncherSettings()) {
+        if (BuildCompat.isAtLeastV()
+                && com.android.launcher3.Flags.enableTwoPaneLauncherSettings()) {
             RuleController.getInstance(this).setRules(
                     RuleController.parseRules(this, R.xml.split_configuration));
         }
@@ -1082,7 +1081,6 @@
 
     @Override
     protected void onStart() {
-        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onStart: instance=" + this);
         TraceHelper.INSTANCE.beginSection(ON_START_EVT);
         super.onStart();
         if (!mDeferOverlayCallbacks) {
@@ -1096,7 +1094,6 @@
     @Override
     @CallSuper
     protected void onDeferredResumed() {
-        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onDeferredResumed: instance=" + this);
         logStopAndResume(true /* isResume */);
 
         // Process any items that were added while Launcher was away.
@@ -1268,11 +1265,7 @@
         }
 
         // Set screen title for Talkback
-        if (state == ALL_APPS) {
-            setTitle(R.string.all_apps_label);
-        } else {
-            setTitle(R.string.home_screen);
-        }
+        setTitle(state.getTitle());
     }
 
     /**
@@ -1284,7 +1277,6 @@
 
     @Override
     protected void onResume() {
-        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onResume: instance=" + this);
         TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT);
         super.onResume();
 
@@ -1300,7 +1292,6 @@
 
     @Override
     protected void onPause() {
-        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onPause: instance=" + this);
         // Ensure that items added to Launcher are queued until Launcher returns
         ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_ACTIVITY_PAUSED);
 
@@ -1783,7 +1774,6 @@
 
     @Override
     public void onDestroy() {
-        testLogD(CLOCK_ICON_DRAWABLE_LEAKING, "onDestroy: instance=" + this);
         super.onDestroy();
         ACTIVITY_TRACKER.onActivityDestroyed(this);
 
@@ -2793,7 +2783,7 @@
     }
 
     private void updateDisallowBack() {
-        if (Flags.enableDesktopWindowingMode()) {
+        if (BuildCompat.isAtLeastV() && Flags.enableDesktopWindowingMode()) {
             // TODO(b/330183377) disable back in launcher when when we productionize
             return;
         }
@@ -3150,4 +3140,4 @@
     }
 
     // End of Getters and Setters
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index a4ae1c8..3b8ff62 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -39,6 +39,7 @@
 import android.util.Log;
 
 import androidx.annotation.Nullable;
+import androidx.core.os.BuildCompat;
 
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.IconCache;
@@ -107,7 +108,7 @@
         mOnTerminateCallback.add(() ->
                 mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
 
-        if (Flags.enableSupportForArchiving()) {
+        if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
             ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
             params.setEnableUnarchivalConfirmation(false);
             launcherApps.setArchiveCompatibility(params);
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 72a3c53..d2d56f2 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -38,6 +38,7 @@
 import android.view.animation.Interpolator;
 
 import androidx.annotation.FloatRange;
+import androidx.annotation.StringRes;
 
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StateManager;
@@ -369,6 +370,10 @@
         return launcher.getWorkspace().getCurrentPageDescription();
     }
 
+    public @StringRes int getTitle() {
+        return R.string.home_screen;
+    }
+
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
         if ((this != NORMAL && this != HINT_STATE)
                 || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 16630967..98ca420 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -267,13 +267,15 @@
                 PrivateProfileManager privateProfileManager = mApps.getPrivateProfileManager();
                 if (privateProfileManager != null) {
                     // Set the alpha of the private space icon to 0 upon expanding the header so the
-                    // alpha can animate -> 1.
+                    // alpha can animate -> 1. This should only be in effect when doing a
+                    // transitioning between Locked/Unlocked state.
                     boolean isPrivateSpaceItem =
                             privateProfileManager.isPrivateSpaceItem(adapterItem);
                     if (icon.getAlpha() == 0 || icon.getAlpha() == 1) {
                         icon.setAlpha(isPrivateSpaceItem
-                                && (privateProfileManager.getAnimationScrolling() ||
-                                    privateProfileManager.getAnimate())
+                                && privateProfileManager.isStateTransitioning()
+                                && (privateProfileManager.isScrolling() ||
+                                    privateProfileManager.getReadyToAnimate())
                                 && privateProfileManager.getCurrentState() == STATE_ENABLED
                                 ? 0 : 1);
                     }
@@ -298,7 +300,7 @@
             case VIEW_TYPE_PRIVATE_SPACE_HEADER:
                 RelativeLayout psHeaderLayout = holder.itemView.findViewById(
                         R.id.ps_header_layout);
-                mApps.getPrivateProfileManager().addPrivateSpaceHeaderViewElements(psHeaderLayout);
+                mApps.getPrivateProfileManager().bindPrivateSpaceHeaderViewElements(psHeaderLayout);
                 AdapterItem adapterItem = mApps.getAdapterItems().get(position);
                 int roundRegions = ROUND_TOP_LEFT | ROUND_TOP_RIGHT;
                 if (mApps.getPrivateProfileManager().getCurrentState() == STATE_DISABLED) {
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 27340a3..32b5cfa 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -30,7 +30,6 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP;
@@ -52,7 +51,6 @@
 import android.os.UserManager;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
@@ -104,6 +102,7 @@
     private static final int LOCK_TEXT_OPACITY_DELAY = 500;
     private static final int MASK_VIEW_DELAY = 400;
     private static final int NO_DELAY = 0;
+    private static final int CONTAINER_OPACITY_DURATION = 150;
     private final ActivityAllAppsContainerView<?> mAllApps;
     private final Predicate<UserHandle> mPrivateProfileMatcher;
     private final int mPsHeaderHeight;
@@ -114,16 +113,21 @@
         public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
             super.onScrollStateChanged(recyclerView, newState);
             if (newState == RecyclerView.SCROLL_STATE_IDLE) {
-                mAnimationScrolling = false;
+                mIsScrolling = false;
             }
         }
     };
     private Intent mAppInstallerIntent = new Intent();
     private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator;
     private boolean mPrivateSpaceSettingsAvailable;
+    // Returns if the animation is currently running.
     private boolean mIsAnimationRunning;
-    private boolean mAnimate;
-    private boolean mAnimationScrolling;
+    // mAnimate denotes if private space is ready to be animated.
+    private boolean mReadyToAnimate;
+    // Returns when the recyclerView is currently scrolling.
+    private boolean mIsScrolling;
+    // mIsStateTransitioning indicates that private space is transitioning between states.
+    private boolean mIsStateTransitioning;
     private Runnable mOnPSHeaderAdded;
     @Nullable
     private RelativeLayout mPSHeader;
@@ -191,23 +195,6 @@
         mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1);
     }
 
-    /**
-     * Disables quiet mode for Private Space User Profile.
-     * When called from search, a runnable is set and executed in the {@link #reset()} method, when
-     * Launcher receives update about profile availability.
-     * The runnable is only executed once, and reset after execution.
-     * In case the method is called again, before the previously set runnable was executed,
-     * the runnable will be updated.
-     */
-    public void unlockPrivateProfile() {
-        setQuietMode(false);
-    }
-
-    /** Enables quiet mode for Private Space User Profile. */
-    void lockPrivateProfile() {
-        setQuietMode(true);
-    }
-
     /** Whether private profile should be hidden on Launcher. */
     public boolean isPrivateSpaceHidden() {
         return getCurrentState() == STATE_DISABLED && SettingsCache.INSTANCE
@@ -230,38 +217,16 @@
         if (mPSHeader != null) {
             mPSHeader.setAlpha(1);
         }
-        if (transitioningFromLockedToUnlocked(previousState, updatedState)) {
+        // It's possible that previousState is 0 when reset is first called.
+        mIsStateTransitioning = previousState != STATE_UNKNOWN && previousState != updatedState;
+        if (previousState == STATE_DISABLED && updatedState == STATE_ENABLED) {
             postUnlock();
-        } else if (transitioningFromUnlockedToLocked(previousState, updatedState)){
+        } else if (previousState == STATE_ENABLED && updatedState == STATE_DISABLED){
             executeLock();
         }
         resetPrivateSpaceDecorator(updatedState);
     }
 
-    /**
-     * Opens the Private Space Settings Page.
-     *
-     * @param view the view that was clicked to open the settings page and which will be the same
-     *             view to animate back. Otherwise if there is no view, simply start the activity.
-     */
-    public void openPrivateSpaceSettings(View view) {
-        if (mPrivateSpaceSettingsAvailable) {
-            Context context = mAllApps.getContext();
-            Intent intent = ApiWrapper.INSTANCE.get(context).getPrivateSpaceSettingsIntent();
-            if (view == null) {
-                context.startActivity(intent);
-                return;
-            }
-            ActivityContext activityContext = ActivityContext.lookupContext(context);
-            AppInfo itemInfo = new AppInfo();
-            itemInfo.id = CONTAINER_PRIVATESPACE;
-            itemInfo.componentName = intent.getComponent();
-            itemInfo.container = CONTAINER_PRIVATESPACE;
-            view.setTag(itemInfo);
-            activityContext.startActivitySafely(view, intent, itemInfo);
-        }
-    }
-
     /** Returns whether or not Private Space Settings Page is available. */
     public boolean isPrivateSpaceSettingsAvailable() {
         return mPrivateSpaceSettingsAvailable;
@@ -321,7 +286,7 @@
     @Override
     public void setQuietMode(boolean enable) {
         super.setQuietMode(enable);
-        mAnimate = true;
+        mReadyToAnimate = true;
     }
 
     /**
@@ -343,7 +308,7 @@
 
     void setAnimationRunning(boolean isAnimationRunning) {
         if (!isAnimationRunning) {
-            mAnimate = false;
+            mReadyToAnimate = false;
         }
         mIsAnimationRunning = isAnimationRunning;
     }
@@ -352,14 +317,6 @@
         return mIsAnimationRunning;
     }
 
-    private boolean transitioningFromLockedToUnlocked(int previousState, int updatedState) {
-        return previousState == STATE_DISABLED && updatedState == STATE_ENABLED;
-    }
-
-    private boolean transitioningFromUnlockedToLocked(int previousState, int updatedState) {
-        return previousState == STATE_ENABLED && updatedState == STATE_DISABLED;
-    }
-
     @Override
     public Predicate<UserHandle> getUserMatcher() {
         return mPrivateProfileMatcher;
@@ -378,7 +335,7 @@
     }
 
     /** Add Private Space Header view elements based upon {@link UserProfileState} */
-    public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) {
+    public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) {
         mPSHeader = parent;
         if (mOnPSHeaderAdded != null) {
             MAIN_EXECUTOR.execute(mOnPSHeaderAdded);
@@ -386,7 +343,7 @@
         }
         // Set the transition duration for the settings and lock button to animate.
         ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
-        if (mAnimate) {
+        if (mReadyToAnimate) {
             enableLayoutTransition(settingAndLockGroup);
         } else {
             // Ensure any unwanted animations to not happen.
@@ -396,27 +353,27 @@
         //Add quietMode image and action for lock/unlock button
         ViewGroup lockButton = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
         assert lockButton != null;
-        addLockButton(lockButton);
+        updateLockButton(lockButton);
 
         //Trigger lock/unlock action from header.
-        addHeaderOnClickListener(mPSHeader);
+        updateHeaderOnClickListener(mPSHeader);
 
         //Add image and action for private space settings button
-        ImageButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
+        PrivateSpaceSettingsButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
         assert settingsButton != null;
-        addPrivateSpaceSettingsButton(settingsButton);
+        updatePrivateSpaceSettingsButton(settingsButton);
 
         //Add image for private space transitioning view
         ImageView transitionView = parent.findViewById(R.id.ps_transition_image);
         assert transitionView != null;
-        addTransitionImage(transitionView);
+        updateTransitionImage(transitionView);
     }
 
     /**
      *  Adds the quietModeButton and attach onClickListener for the header to animate different
      *  states when clicked.
      */
-    private void addLockButton(ViewGroup lockButton) {
+    private void updateLockButton(ViewGroup lockButton) {
         TextView lockText = lockButton.findViewById(R.id.lock_text);
         switch (getCurrentState()) {
             case STATE_ENABLED -> {
@@ -435,7 +392,7 @@
         }
     }
 
-    private void addHeaderOnClickListener(RelativeLayout header) {
+    private void updateHeaderOnClickListener(RelativeLayout header) {
         if (getCurrentState() == STATE_DISABLED) {
             header.setOnClickListener(view -> lockingAction(/* lock */ false));
             header.setClickable(true);
@@ -453,28 +410,19 @@
     /** Sets the enablement of the profile when header or button is clicked. */
     private void lockingAction(boolean lock) {
         logEvents(lock ? LAUNCHER_PRIVATE_SPACE_LOCK_TAP : LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP);
-        if (lock) {
-            lockPrivateProfile();
-        } else {
-            unlockPrivateProfile();
-        }
+        setQuietMode(lock);
     }
 
-    private void addPrivateSpaceSettingsButton(ImageButton settingsButton) {
+    private void updatePrivateSpaceSettingsButton(PrivateSpaceSettingsButton settingsButton) {
         if (getCurrentState() == STATE_ENABLED
                 && isPrivateSpaceSettingsAvailable()) {
             settingsButton.setVisibility(VISIBLE);
-            settingsButton.setOnClickListener(
-                    view -> {
-                        logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP);
-                        openPrivateSpaceSettings(view);
-                    });
         } else {
             settingsButton.setVisibility(GONE);
         }
     }
 
-    private void addTransitionImage(ImageView transitionImage) {
+    private void updateTransitionImage(ImageView transitionImage) {
         if (getCurrentState() == STATE_TRANSITION) {
             transitionImage.setVisibility(VISIBLE);
         } else {
@@ -498,7 +446,10 @@
                                 return LinearSmoothScroller.SNAP_TO_END;
                             }
                         };
-                smoothScroller.setTargetPosition(i);
+                // If privateSpaceHidden() then the entire container decorator will be invisible and
+                // we can directly move to an element above the header. There should always be one
+                // element, as PS is present in the bottom of All Apps.
+                smoothScroller.setTargetPosition(isPrivateSpaceHidden() ? i - 1 : i);
                 RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager();
                 if (layoutManager != null) {
                     startAnimationScroll(allAppsRecyclerView, layoutManager, smoothScroller);
@@ -620,8 +571,11 @@
                 float newAlpha = (float) valueAnimator.getAnimatedValue();
                 for (int i = 0; i < allAppsAdapterItems.size(); i++) {
                     BaseAllAppsAdapter.AdapterItem currentItem = allAppsAdapterItems.get(i);
+                    // When not hidden: Fade all PS items except header.
+                    // When hidden: Fade all items.
                     if (isPrivateSpaceItem(currentItem) &&
-                            currentItem.viewType != VIEW_TYPE_PRIVATE_SPACE_HEADER) {
+                            (currentItem.viewType != VIEW_TYPE_PRIVATE_SPACE_HEADER
+                                    || isPrivateSpaceHidden())) {
                         RecyclerView.ViewHolder viewHolder =
                                 allAppsRecyclerView.findViewHolderForAdapterPosition(i);
                         if (viewHolder != null) {
@@ -681,6 +635,7 @@
             }
         });
         animatorSet.addListener(forEndCallback(() -> {
+            mIsStateTransitioning = false;
             setAnimationRunning(false);
             getMainRecyclerView().setChildAttachedConsumer(child -> child.setAlpha(1));
             mStatsLogManager.logger().sendToInteractionJankMonitor(
@@ -702,43 +657,42 @@
                     translateFloatingMaskView(false));
         } else {
             if (isPrivateSpaceHidden()) {
-                animatorSet.playSequentially(translateFloatingMaskView(false),
-                        animateAlphaOfIcons(false),
-                        animateCollapseAnimation(),
-                        fadeOutHeaderAlpha());
+                animatorSet.playSequentially(animateAlphaOfIcons(false),
+                        animateAlphaOfPrivateSpaceContainer(),
+                        animateCollapseAnimation());
             } else {
                 animatorSet.playSequentially(translateFloatingMaskView(true),
                         animateAlphaOfIcons(false),
                         animateCollapseAnimation());
             }
         }
-        animatorSet.setDuration(EXPAND_COLLAPSE_DURATION);
         animatorSet.start();
     }
 
-    /** Fades out the private space container. */
-    private ValueAnimator fadeOutHeaderAlpha() {
-        if (mPSHeader == null) {
-            return new ValueAnimator();
-        }
-        float from = 1;
-        float to = 0;
-        ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
-        alphaAnim.setDuration(EXPAND_COLLAPSE_DURATION);
-        alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                if (mPSHeader != null) {
-                    mPSHeader.setAlpha((float) valueAnimator.getAnimatedValue());
+    /** Fades out the private space container (defined by its items' decorators). */
+    private ValueAnimator animateAlphaOfPrivateSpaceContainer() {
+        int from = 255; // 100% opacity.
+        int to = 0; // No opacity.
+        ValueAnimator alphaAnim = ObjectAnimator.ofInt(from, to);
+        AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView();
+        List<BaseAllAppsAdapter.AdapterItem> allAppsAdapterItems =
+                allAppsRecyclerView.getApps().getAdapterItems();
+        alphaAnim.setDuration(CONTAINER_OPACITY_DURATION);
+        alphaAnim.addUpdateListener(valueAnimator -> {
+            for (BaseAllAppsAdapter.AdapterItem currentItem : allAppsAdapterItems) {
+                if (isPrivateSpaceItem(currentItem)) {
+                    currentItem.setDecorationFillAlpha((int) valueAnimator.getAnimatedValue());
                 }
             }
+            // Invalidate the parent view, to redraw the decorations with changed alpha.
+            allAppsRecyclerView.invalidate();
         });
         return alphaAnim;
     }
 
     /** Fades out the private space container. */
     private ValueAnimator translateFloatingMaskView(boolean animateIn) {
-        if (!Flags.privateSpaceFloatingMaskView() || mFloatingMaskView == null) {
+        if (!Flags.privateSpaceAddFloatingMaskView() || mFloatingMaskView == null) {
             return new ValueAnimator();
         }
         // Translate base on the height amount. Translates out on expand and in on collapse.
@@ -773,7 +727,7 @@
             public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
                     View view, int i) {
                 settingsAndLockGroup.setLayoutTransition(null);
-                mAnimate = false;
+                mReadyToAnimate = false;
             }
         });
         settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition);
@@ -849,7 +803,7 @@
     }
 
     private void attachFloatingMaskView(boolean expand) {
-        if (!Flags.privateSpaceFloatingMaskView()) {
+        if (!Flags.privateSpaceAddFloatingMaskView()) {
             return;
         }
         mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate(
@@ -873,7 +827,7 @@
     /** Starts the smooth scroll with the provided smoothScroller and add idle listener. */
     private void startAnimationScroll(AllAppsRecyclerView allAppsRecyclerView,
             RecyclerView.LayoutManager layoutManager, RecyclerView.SmoothScroller smoothScroller) {
-        mAnimationScrolling = true;
+        mIsScrolling = true;
         layoutManager.startSmoothScroll(smoothScroller);
         allAppsRecyclerView.removeOnScrollListener(mOnIdleScrollListener);
         allAppsRecyclerView.addOnScrollListener(mOnIdleScrollListener);
@@ -887,12 +841,24 @@
         return mAllApps.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN).mRecyclerView;
     }
 
-    boolean getAnimate() {
-        return mAnimate;
+    /** Returns if private space is readily available to be animated. */
+    boolean getReadyToAnimate() {
+        return mReadyToAnimate;
     }
 
-    boolean getAnimationScrolling() {
-        return mAnimationScrolling;
+    /** Returns when a smooth scroll is happening. */
+    boolean isScrolling() {
+        return mIsScrolling;
+    }
+
+    /**
+     * Returns when private space is in the process of transitioning. This is different from
+     * getAnimate() since mStateTransitioning checks from the time transitioning starts happening
+     * in reset() as oppose to when private space is animating. This should be used to ensure
+     * Private Space state during onBind().
+     */
+    boolean isStateTransitioning() {
+        return mIsStateTransitioning;
     }
 
     int getPsHeaderHeight() {
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceSettingsButton.java b/src/com/android/launcher3/allapps/PrivateSpaceSettingsButton.java
new file mode 100644
index 0000000..43e42ff
--- /dev/null
+++ b/src/com/android/launcher3/allapps/PrivateSpaceSettingsButton.java
@@ -0,0 +1,81 @@
+/*
+ * 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.allapps;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PRIVATESPACE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.util.ApiWrapper;
+import com.android.launcher3.views.ActivityContext;
+
+public class PrivateSpaceSettingsButton extends ImageButton implements View.OnClickListener {
+
+    private final ActivityContext mActivityContext;
+    private final StatsLogManager mStatsLogManager;
+    private final Intent mPrivateSpaceSettingsIntent;
+
+    public PrivateSpaceSettingsButton(Context context) {
+        this(context, null, 0);
+    }
+
+    public PrivateSpaceSettingsButton(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PrivateSpaceSettingsButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mActivityContext = ActivityContext.lookupContext(context);
+        mStatsLogManager = mActivityContext.getStatsLogManager();
+        mPrivateSpaceSettingsIntent =
+                ApiWrapper.INSTANCE.get(context).getPrivateSpaceSettingsIntent();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View view) {
+        mStatsLogManager.logger().log(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP);
+        AppInfo privateSpaceSettingsItemInfo = createPrivateSpaceSettingsAppInfo();
+        view.setTag(privateSpaceSettingsItemInfo);
+        mActivityContext.startActivitySafely(
+                view,
+                mPrivateSpaceSettingsIntent,
+                privateSpaceSettingsItemInfo);
+    }
+
+    AppInfo createPrivateSpaceSettingsAppInfo() {
+        AppInfo itemInfo = new AppInfo();
+        itemInfo.id = CONTAINER_PRIVATESPACE;
+        if (mPrivateSpaceSettingsIntent != null) {
+            itemInfo.componentName = mPrivateSpaceSettingsIntent.getComponent();
+        }
+        itemInfo.container = CONTAINER_PRIVATESPACE;
+        return itemInfo;
+    }
+}
diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java
index 3351ee3..eb74d20 100644
--- a/src/com/android/launcher3/allapps/UserProfileManager.java
+++ b/src/com/android/launcher3/allapps/UserProfileManager.java
@@ -40,11 +40,13 @@
  * {@link PrivateProfileManager} which manages private profile state.
  */
 public abstract class UserProfileManager {
+    public static final int STATE_UNKNOWN = 0;
     public static final int STATE_ENABLED = 1;
     public static final int STATE_DISABLED = 2;
     public static final int STATE_TRANSITION = 3;
 
     @IntDef(value = {
+            STATE_UNKNOWN,
             STATE_ENABLED,
             STATE_DISABLED,
             STATE_TRANSITION
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index ec0a222..85eb39b 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -91,7 +91,8 @@
  */
 @TargetApi(Build.VERSION_CODES.O)
 public class AddItemActivity extends BaseActivity
-        implements OnLongClickListener, OnTouchListener, AbstractSlideInView.OnCloseListener {
+        implements OnLongClickListener, OnTouchListener, AbstractSlideInView.OnCloseListener,
+        WidgetCell.PreviewReadyListener {
 
     private static final int SHADOW_SIZE = 10;
 
@@ -142,6 +143,7 @@
         mDragLayer = findViewById(R.id.add_item_drag_layer);
         mDragLayer.recreateControllers();
         mWidgetCell = findViewById(R.id.widget_cell);
+        mWidgetCell.addPreviewReadyListener(this);
         mAccessibilityManager =
                 getApplicationContext().getSystemService(AccessibilityManager.class);
 
@@ -454,4 +456,11 @@
                 .withItemInfo((ItemInfo) mWidgetCell.getWidgetView().getTag())
                 .log(command);
     }
+
+    @Override
+    public void onPreviewAvailable() {
+        // Set the preview height based on "the only" widget's preview.
+        mWidgetCell.setParentAlignedPreviewHeight(mWidgetCell.getPreviewContentHeight());
+        mWidgetCell.post(mWidgetCell::requestLayout);
+    }
 }
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
index f3708a2..29fc613 100644
--- a/src/com/android/launcher3/dragndrop/LauncherDragController.java
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -28,6 +28,7 @@
 import android.view.View;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DragSource;
@@ -36,6 +37,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.widget.util.WidgetDragScaleUtils;
 
 /**
  * Drag controller for Launcher activity
@@ -43,7 +45,6 @@
 public class LauncherDragController extends DragController<Launcher> {
 
     private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
-
     private final FlingToDeleteHelper mFlingToDeleteHelper;
 
     public LauncherDragController(Launcher launcher) {
@@ -92,8 +93,13 @@
                 && !mOptions.preDragCondition.shouldStartDrag(0);
 
         final Resources res = mActivity.getResources();
-        final float scaleDps = mIsInPreDrag
-                ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
+
+        final float scalePx;
+        if (originalView.getViewType() == DraggableView.DRAGGABLE_WIDGET) {
+            scalePx = mIsInPreDrag ? 0f : getWidgetDragScalePx(drawable, view, dragInfo);
+        } else {
+            scalePx = mIsInPreDrag ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
+        }
         final DragView dragView = mDragObject.dragView = drawable != null
                 ? new LauncherDragView(
                 mActivity,
@@ -102,7 +108,7 @@
                 registrationY,
                 initialDragViewScale,
                 dragViewScaleOnDrop,
-                scaleDps)
+                scalePx)
                 : new LauncherDragView(
                         mActivity,
                         view,
@@ -112,7 +118,7 @@
                         registrationY,
                         initialDragViewScale,
                         dragViewScaleOnDrop,
-                        scaleDps);
+                        scalePx);
         dragView.setItemInfo(dragInfo);
         mDragObject.dragComplete = false;
 
@@ -157,6 +163,29 @@
         return dragView;
     }
 
+
+    /**
+     * Returns the scale in terms of pixels (to be applied on width) to scale the preview
+     * during drag and drop.
+     */
+    @VisibleForTesting
+    float getWidgetDragScalePx(@Nullable Drawable drawable, @Nullable View view,
+            ItemInfo dragInfo) {
+        float draggedViewWidthPx = 0;
+        float draggedViewHeightPx = 0;
+
+        if (view != null) {
+            draggedViewWidthPx = view.getMeasuredWidth();
+            draggedViewHeightPx = view.getMeasuredHeight();
+        } else if (drawable != null) {
+            draggedViewWidthPx = drawable.getIntrinsicWidth();
+            draggedViewHeightPx = drawable.getIntrinsicHeight();
+        }
+
+        return WidgetDragScaleUtils.getWidgetDragScalePx(mActivity, mActivity.getDeviceProfile(),
+                draggedViewWidthPx, draggedViewHeightPx, dragInfo);
+    }
+
     @Override
     protected void exitDrag() {
         if (!mActivity.isInState(EDIT_MODE)) {
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 04d8ac0..44e448e 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -57,6 +57,7 @@
 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.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -641,4 +642,10 @@
 
         void reapplyItemInfo(ItemInfoWithIcon info);
     }
+
+    /** Log persistently to FileLog.d for debugging. */
+    @Override
+    protected void logdPersistently(String tag, String message, @Nullable Exception e) {
+        FileLog.d(tag, message, e);
+    }
 }
diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 8e53aff..d6b41b0 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -61,7 +61,7 @@
  */
 public class QsbContainerView extends FrameLayout {
 
-    public static final String SEARCH_PROVIDER_SETTINGS_KEY = "SEARCH_PROVIDER_PACKAGE_NAME";
+    public static final String SEARCH_ENGINE_SETTINGS_KEY = "selected_search_engine";
 
     /**
      * Returns the package name for user configured search provider or from searchManager
@@ -71,8 +71,8 @@
     @WorkerThread
     @Nullable
     public static String getSearchWidgetPackageName(@NonNull Context context) {
-        String providerPkg = Settings.Global.getString(context.getContentResolver(),
-                SEARCH_PROVIDER_SETTINGS_KEY);
+        String providerPkg = Settings.Secure.getString(context.getContentResolver(),
+                SEARCH_ENGINE_SETTINGS_KEY);
         if (providerPkg == null) {
             SearchManager searchManager = context.getSystemService(SearchManager.class);
             ComponentName componentName = searchManager.getGlobalSearchActivity();
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index cdbd0c0..df8f635 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -109,13 +109,6 @@
 
     private float mLastTouchY;
     private boolean mIsDragging;
-    /**
-     * Tracks whether a keyboard hide request has been sent due to downward scrolling.
-     * <p>
-     * Set to true when scrolling down and reset when scrolling up to prevents redundant hide
-     * requests during continuous downward scrolls.
-     */
-    private boolean mRequestedHideKeyboard;
     private boolean mIsThumbDetached;
     private final boolean mCanThumbDetach;
     private boolean mIgnoreDragGesture;
@@ -248,7 +241,6 @@
     public boolean handleTouchEvent(MotionEvent ev, Point offset) {
         int x = (int) ev.getX() - offset.x;
         int y = (int) ev.getY() - offset.y;
-        ActivityContext activityContext = ActivityContext.lookupContext(getContext());
 
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN:
@@ -256,7 +248,6 @@
                 mDownX = x;
                 mDownY = mLastY = y;
                 mDownTimeStampMillis = ev.getDownTime();
-                mRequestedHideKeyboard = false;
 
                 if ((Math.abs(mDy) < mDeltaThreshold &&
                         mRv.getScrollState() != SCROLL_STATE_IDLE)) {
@@ -269,15 +260,6 @@
                 }
                 break;
             case MotionEvent.ACTION_MOVE:
-                if (y > mLastY) {
-                    if (!mRequestedHideKeyboard) {
-                        activityContext.hideKeyboard();
-                    }
-                    mRequestedHideKeyboard = true;
-                } else {
-                    mRequestedHideKeyboard = false;
-                }
-
                 mLastY = y;
                 int absDeltaY = Math.abs(y - mDownY);
                 int absDeltaX = Math.abs(x - mDownX);
@@ -312,6 +294,7 @@
     }
 
     private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
+        ActivityContext.lookupContext(getContext()).hideKeyboard();
         mIsDragging = true;
         if (mCanThumbDetach) {
             mIsThumbDetached = true;
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index eac2ce7..2bb485a 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -103,6 +103,8 @@
     private Size mWidgetSize;
 
     private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
+    @Nullable
+    private PreviewReadyListener mPreviewReadyListener = null;
 
     protected CancellableTask mActiveRequest;
     private boolean mAnimatePreview = true;
@@ -118,7 +120,8 @@
 
     private CancellableTask mIconLoadRequest;
     private boolean mIsShowingAddButton = false;
-
+    // Height enforced by the parent to align all widget cells displayed by it.
+    private int mParentAlignedPreviewHeight;
     public WidgetCell(Context context) {
         this(context, null);
     }
@@ -190,6 +193,8 @@
         mWidgetDims.setText(null);
         mWidgetDescription.setText(null);
         mWidgetDescription.setVisibility(GONE);
+        mPreviewReadyListener = null;
+        mParentAlignedPreviewHeight = 0;
         showDescription(true);
         showDimensions(true);
 
@@ -338,8 +343,8 @@
 
     private void updateAppWidgetHostScale(NavigableAppWidgetHostView view) {
         // Scale the content such that all of the content is visible
-        int contentWidth = view.getWidth();
-        int contentHeight = view.getHeight();
+        float contentWidth = view.getWidth();
+        float contentHeight = view.getHeight();
 
         if (view.getChildCount() == 1) {
             View content = view.getChildAt(0);
@@ -359,6 +364,12 @@
             mAppWidgetHostViewScale = Math.min(pWidth / contentWidth, pHeight / contentHeight);
         }
         view.setScaleToFit(mAppWidgetHostViewScale);
+
+        // layout based previews maybe ready at this point to inspect their inner height.
+        if (mPreviewReadyListener != null) {
+            mPreviewReadyListener.onPreviewAvailable();
+            mPreviewReadyListener = null;
+        }
     }
 
     public WidgetImageView getWidgetView() {
@@ -384,6 +395,12 @@
                 removeView(mAppWidgetHostViewPreview);
                 mAppWidgetHostViewPreview = null;
             }
+
+            // Drawables of the image previews are available at this point to measure.
+            if (mPreviewReadyListener != null) {
+                mPreviewReadyListener.onPreviewAvailable();
+                mPreviewReadyListener = null;
+            }
         }
 
         if (mAnimatePreview) {
@@ -489,14 +506,20 @@
         // mPreviewContainerScale ensures the needed scaling with respect to original widget size.
         mAppWidgetHostViewScale = mPreviewContainerScale;
         containerLp.width = mPreviewContainerSize.getWidth();
-        containerLp.height = mPreviewContainerSize.getHeight();
+        int height = mPreviewContainerSize.getHeight();
 
         // If we don't have enough available width, scale the preview container to fit.
         if (containerLp.width > maxWidth) {
             containerLp.width = maxWidth;
             mAppWidgetHostViewScale = (float) containerLp.width / mPreviewContainerSize.getWidth();
-            containerLp.height = Math.round(
-                    mPreviewContainerSize.getHeight() * mAppWidgetHostViewScale);
+            height = Math.round(mPreviewContainerSize.getHeight() * mAppWidgetHostViewScale);
+        }
+
+        // Use parent aligned height in set.
+        if (mParentAlignedPreviewHeight > 0) {
+            containerLp.height = Math.min(height, mParentAlignedPreviewHeight);
+        } else {
+            containerLp.height = height;
         }
 
         // No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass
@@ -513,6 +536,42 @@
     }
 
     /**
+     * Sets the height of the preview as adjusted by the parent to have this cell's content aligned
+     * with other cells displayed by the parent.
+     */
+    public void setParentAlignedPreviewHeight(int previewHeight) {
+        mParentAlignedPreviewHeight = previewHeight;
+    }
+
+    /**
+     * Returns the height of the preview without any empty space.
+     * In case of appwidget host views, it returns the height of first child. This way, if preview
+     * view provided by an app doesn't fill bounds, this will return actual height without white
+     * space.
+     */
+    public int getPreviewContentHeight() {
+        // By default assume scaled height.
+        int height = Math.round(mPreviewContainerScale * mWidgetSize.getHeight());
+
+        if (mWidgetImage != null && mWidgetImage.getDrawable() != null) {
+            // getBitmapBounds returns the scaled bounds.
+            Rect bitmapBounds = mWidgetImage.getBitmapBounds();
+            height = bitmapBounds.height();
+        } else if (mAppWidgetHostViewPreview != null
+                && mAppWidgetHostViewPreview.getChildCount() == 1) {
+            int contentHeight = Math.round(
+                    mPreviewContainerScale * mWidgetSize.getHeight());
+            int previewInnerHeight = Math.round(
+                    mAppWidgetHostViewScale * mAppWidgetHostViewPreview.getChildAt(
+                            0).getMeasuredHeight());
+            // Use either of the inner scaled height or the scaled widget height
+            height = Math.min(contentHeight, previewInnerHeight);
+        }
+
+        return height;
+    }
+
+    /**
      * Loads a high resolution package icon to show next to the widget title.
      */
     public void loadHighResPackageIcon() {
@@ -651,4 +710,19 @@
         }
         return false;
     }
+
+    /**
+     * Listener to notify when previews are available.
+     */
+    public void addPreviewReadyListener(PreviewReadyListener previewReadyListener) {
+        mPreviewReadyListener = previewReadyListener;
+    }
+
+    /**
+     * Listener interface for subscribers to listen to preview's availability.
+     */
+    public interface PreviewReadyListener {
+        /** Handler on to invoke when previews are available. */
+        void onPreviewAvailable();
+    }
 }
diff --git a/src/com/android/launcher3/widget/WidgetTableRow.java b/src/com/android/launcher3/widget/WidgetTableRow.java
new file mode 100644
index 0000000..a5312ce
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetTableRow.java
@@ -0,0 +1,90 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TableRow;
+
+/**
+ * A row of {@link WidgetCell}s that can be displayed in a table.
+ */
+public class WidgetTableRow extends TableRow implements WidgetCell.PreviewReadyListener {
+    private int mNumOfReadyCells;
+    private int mNumOfCells;
+    private int mResizeDelay;
+
+    public WidgetTableRow(Context context) {
+        super(context);
+    }
+    public WidgetTableRow(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void onPreviewAvailable() {
+        mNumOfReadyCells++;
+
+        // Once all previews are loaded, find max visible height and adjust the preview containers.
+        if (mNumOfReadyCells == mNumOfCells) {
+            resize();
+        }
+    }
+
+    private void resize() {
+        int previewHeight = 0;
+        // get the maximum height of each widget preview
+        for (int i = 0; i < getChildCount(); i++) {
+            WidgetCell widgetCell = (WidgetCell) getChildAt(i);
+            previewHeight = Math.max(widgetCell.getPreviewContentHeight(), previewHeight);
+        }
+        if (mResizeDelay > 0) {
+            postDelayed(() -> setAlpha(1f), mResizeDelay);
+        }
+        if (previewHeight > 0) {
+            for (int i = 0; i < getChildCount(); i++) {
+                WidgetCell widgetCell = (WidgetCell) getChildAt(i);
+                widgetCell.setParentAlignedPreviewHeight(previewHeight);
+                widgetCell.postDelayed(widgetCell::requestLayout, mResizeDelay);
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+    }
+
+    /**
+     * Sets up the row to display the provided number of numOfCells.
+     *
+     * @param numOfCells    number of numOfCells in the row
+     * @param resizeDelayMs time to wait in millis before making any layout size adjustments e.g. we
+     *                      want to wait for list expand collapse animation before resizing the
+     *                      cell previews.
+     */
+    public void setupRow(int numOfCells, int resizeDelayMs) {
+        mNumOfCells = numOfCells;
+        mNumOfReadyCells = 0;
+
+        mResizeDelay = resizeDelayMs;
+        // For delayed resize, reveal contents only after resize is done.
+        if (mResizeDelay > 0) {
+            setAlpha(0);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 4ea2426..894099d 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -31,7 +31,6 @@
 import android.view.animation.Interpolator;
 import android.widget.ScrollView;
 import android.widget.TableLayout;
-import android.widget.TableRow;
 import android.widget.TextView;
 
 import androidx.annotation.Px;
@@ -137,8 +136,9 @@
                 mActivityContext.getDeviceProfile(), mMaxHorizontalSpan,
                 mWidgetCellHorizontalPadding)
                 .forEach(row -> {
-                    TableRow tableRow = new TableRow(getContext());
+                    WidgetTableRow tableRow = new WidgetTableRow(getContext());
                     tableRow.setGravity(Gravity.TOP);
+                    tableRow.setupRow(row.size(), /*resizeDelayMs=*/ 0);
                     row.forEach(widgetItem -> {
                         WidgetCell widget = addItemCell(tableRow);
                         widget.applyFromCellItem(widgetItem);
@@ -163,9 +163,10 @@
         return super.onControllerInterceptTouchEvent(ev);
     }
 
-    protected WidgetCell addItemCell(ViewGroup parent) {
+    protected WidgetCell addItemCell(WidgetTableRow parent) {
         WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext())
                 .inflate(R.layout.widget_cell, parent, false);
+        widget.addPreviewReadyListener(parent);
         widget.setOnClickListener(this);
 
         View previewContainer = widget.findViewById(R.id.widget_preview_container);
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 4f73e66..9260af9 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -53,6 +53,7 @@
  */
 public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots> {
     private @Px float mAvailableHeight = Float.MAX_VALUE;
+    private @Px float mAvailableWidth = 0;
     private static final String INITIALLY_DISPLAYED_WIDGETS_STATE_KEY =
             "widgetRecommendationsView:mDisplayedWidgets";
     private static final int MAX_CATEGORIES = 3;
@@ -152,6 +153,7 @@
             final @Px float availableHeight, final @Px int availableWidth,
             final @Px int cellPadding) {
         this.mAvailableHeight = availableHeight;
+        this.mAvailableWidth = availableWidth;
         clear();
 
         Set<ComponentName> displayedWidgets = maybeDisplayInTable(recommendedWidgets,
@@ -187,6 +189,7 @@
             DeviceProfile deviceProfile, final @Px float availableHeight,
             final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) {
         this.mAvailableHeight = availableHeight;
+        this.mAvailableWidth = availableWidth;
         Context context = getContext();
         // For purpose of recommendations section, we don't want paging dots to be halved in two
         // pane display, so, we always provide isTwoPanels = "false".
@@ -280,17 +283,24 @@
         boolean hasMultiplePages = getChildCount() > 0;
 
         if (hasMultiplePages) {
-            int finalWidth = MeasureSpec.getSize(widthMeasureSpec);
             int desiredHeight = 0;
+            int desiredWidth = Math.round(mAvailableWidth);
 
             for (int i = 0; i < getChildCount(); i++) {
                 View child = getChildAt(i);
-                measureChild(child, widthMeasureSpec, heightMeasureSpec);
+                // Measure children based on available height and width.
+                measureChild(child,
+                        MeasureSpec.makeMeasureSpec(desiredWidth, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(Math.round(mAvailableHeight),
+                                MeasureSpec.AT_MOST));
                 // Use height of tallest child as we have limited height.
-                desiredHeight = Math.max(desiredHeight, child.getMeasuredHeight());
+                int childHeight = child.getMeasuredHeight();
+                desiredHeight = Math.max(desiredHeight, childHeight);
             }
 
             int finalHeight = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0);
+            int finalWidth = resolveSizeAndState(desiredWidth, widthMeasureSpec, 0);
+
             setMeasuredDimension(finalWidth, finalHeight);
         } else {
             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 6aaa7d2..3be6faa 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
-import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_LIST;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -1023,35 +1022,7 @@
                 default:
                     break;
             }
-            mWidgetsListItemAnimator = new DefaultItemAnimator() {
-                @Override
-                public boolean animateChange(RecyclerView.ViewHolder oldHolder,
-                        RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft,
-                        int toTop) {
-                    // As we expand an item, the content / widgets list that appears (with add
-                    // event) also gets change events as its previews load asynchronously. The
-                    // super implementation of animateChange cancels the animations on it - breaking
-                    // the "add animation". Instead, here, we skip "change" animation for content
-                    // list - because we want it to either appear or disappear. And, the previews
-                    // themselves have their own animation when loaded, so, we don't need change
-                    // animations for them anyway. Below, we do-nothing.
-                    if (oldHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
-                        dispatchChangeStarting(oldHolder, true);
-                        dispatchChangeFinished(oldHolder, true);
-                        return true;
-                    }
-                    return super.animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft,
-                            toTop);
-                }
-            };
-            // Disable change animations because it disrupts the item focus upon adapter item
-            // change.
-            mWidgetsListItemAnimator.setSupportsChangeAnimations(false);
-            // Make the moves a bit faster, so that the amount of time for which user sees the
-            // bottom-sheet background before "add" animation starts is less making it smoother.
-            mWidgetsListItemAnimator.setChangeDuration(90);
-            mWidgetsListItemAnimator.setMoveDuration(90);
-            mWidgetsListItemAnimator.setAddDuration(300);
+            mWidgetsListItemAnimator = new WidgetsListItemAnimator();
         }
 
         private int getEmptySpaceHeight() {
@@ -1065,7 +1036,7 @@
             mWidgetsRecyclerView.setClipChildren(false);
             mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
             mWidgetsRecyclerView.bindFastScrollbar(mFastScroller);
-            mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator);
+            mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator);
             mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
             if (!isTwoPane()) {
                 mWidgetsRecyclerView.setEdgeEffectFactory(
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index d373a3b..d164dd0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.widget.picker;
 
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
@@ -201,7 +203,7 @@
     public void reapplyItemInfo(ItemInfoWithIcon info) {
         if (getTag() == info) {
             mIconLoadRequest = null;
-            mEnableIconUpdateAnimation = true;
+            mEnableIconUpdateAnimation = areAnimatorsEnabled();
 
             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
             info.bitmap.icon.prepareToDraw();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java
new file mode 100644
index 0000000..854700f
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListItemAnimator.java
@@ -0,0 +1,60 @@
+/*
+ * 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.widget.picker;
+
+import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_LIST;
+
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class WidgetsListItemAnimator extends DefaultItemAnimator {
+    public static final int CHANGE_DURATION_MS = 90;
+    public static final int MOVE_DURATION_MS = 90;
+    public static final int ADD_DURATION_MS = 120;
+
+    public WidgetsListItemAnimator() {
+        super();
+
+        // Disable change animations because it disrupts the item focus upon adapter item
+        // change.
+        setSupportsChangeAnimations(false);
+        // Make the moves a bit faster, so that the amount of time for which user sees the
+        // bottom-sheet background before "add" animation starts is less making it smoother.
+        setChangeDuration(CHANGE_DURATION_MS);
+        setMoveDuration(MOVE_DURATION_MS);
+        setAddDuration(ADD_DURATION_MS);
+    }
+    @Override
+    public boolean animateChange(RecyclerView.ViewHolder oldHolder,
+            RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft,
+            int toTop) {
+        // As we expand an item, the content / widgets list that appears (with add
+        // event) also gets change events as its previews load asynchronously. The
+        // super implementation of animateChange cancels the animations on it - breaking
+        // the "add animation". Instead, here, we skip "change" animation for content
+        // list - because we want it to either appear or disappear. And, the previews
+        // themselves have their own animation when loaded, so, we don't need change
+        // animations for them anyway. Below, we do-nothing.
+        if (oldHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
+            dispatchChangeStarting(oldHolder, true);
+            dispatchChangeFinished(oldHolder, true);
+            return true;
+        }
+        return super.animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft,
+                toTop);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 56352cc..45d733a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -15,6 +15,11 @@
  */
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.CHANGE_DURATION_MS;
+import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.MOVE_DURATION_MS;
+
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.util.Log;
@@ -26,7 +31,6 @@
 import android.view.View.OnLongClickListener;
 import android.view.ViewGroup;
 import android.widget.TableLayout;
-import android.widget.TableRow;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Px;
@@ -36,6 +40,7 @@
 import com.android.launcher3.recyclerview.ViewHolderBinder;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetTableRow;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.util.WidgetsTableUtils;
 
@@ -111,17 +116,15 @@
         for (int i = 0; i < widgetItemsTable.size(); i++) {
             List<WidgetItem> widgetItemsPerRow = widgetItemsTable.get(i);
             for (int j = 0; j < widgetItemsPerRow.size(); j++) {
-                TableRow row = (TableRow) table.getChildAt(i);
+                WidgetTableRow row = (WidgetTableRow) table.getChildAt(i);
                 row.setVisibility(View.VISIBLE);
                 WidgetCell widget = (WidgetCell) row.getChildAt(j);
                 widget.clear();
+                widget.addPreviewReadyListener(row);
                 WidgetItem widgetItem = widgetItemsPerRow.get(j);
                 widget.setVisibility(View.VISIBLE);
 
-                // When preview loads, notify adapter to rebind the item and possibly animate
-                widget.applyFromCellItem(widgetItem,
-                        bitmap -> holder.onPreviewLoaded(Pair.create(widgetItem, bitmap)),
-                        holder.previewCache.get(widgetItem));
+                widget.applyFromCellItem(widgetItem);
                 widget.requestLayout();
             }
         }
@@ -143,14 +146,19 @@
 
         for (int i = 0; i < widgetItemsTable.size(); i++) {
             List<WidgetItem> widgetItems = widgetItemsTable.get(i);
-            TableRow tableRow;
+            WidgetTableRow tableRow;
             if (i < table.getChildCount()) {
-                tableRow = (TableRow) table.getChildAt(i);
+                tableRow = (WidgetTableRow) table.getChildAt(i);
             } else {
-                tableRow = new TableRow(table.getContext());
+                tableRow = new WidgetTableRow(table.getContext());
                 tableRow.setGravity(Gravity.TOP);
                 table.addView(tableRow);
             }
+            // Pass resize delay to let the "move" and "change" animations run before resizing the
+            // row.
+            tableRow.setupRow(widgetItems.size(),
+                    /*resizeDelayMs=*/
+                    areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0);
             if (tableRow.getChildCount() > widgetItems.size()) {
                 for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
                     tableRow.getChildAt(j).setVisibility(View.GONE);
@@ -161,6 +169,7 @@
                             R.layout.widget_cell, tableRow, false);
                     // set up touch.
                     widget.setOnClickListener(mIconClickListener);
+                    widget.addPreviewReadyListener(tableRow);
                     View preview = widget.findViewById(R.id.widget_preview_container);
                     preview.setOnClickListener(mIconClickListener);
                     preview.setOnLongClickListener(mIconLongClickListener);
@@ -176,7 +185,7 @@
         int numOfRows = holder.tableContainer.getChildCount();
         holder.previewCache.clear();
         for (int i = 0; i < numOfRows; i++) {
-            TableRow tableRow = (TableRow) holder.tableContainer.getChildAt(i);
+            WidgetTableRow tableRow = (WidgetTableRow) holder.tableContainer.getChildAt(i);
             int numOfCols = tableRow.getChildCount();
             for (int j = 0; j < numOfCols; j++) {
                 WidgetCell widget = (WidgetCell) tableRow.getChildAt(j);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 6dbad5c..1ed3d88 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -27,9 +27,7 @@
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.TableLayout;
-import android.widget.TableRow;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
@@ -38,6 +36,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetTableRow;
 import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize;
 
 import java.util.ArrayList;
@@ -105,7 +104,8 @@
 
         for (int i = 0; i < recommendationTable.size(); i++) {
             List<WidgetItem> widgetItems = recommendationTable.get(i);
-            TableRow tableRow = new TableRow(getContext());
+            WidgetTableRow tableRow = new WidgetTableRow(getContext());
+            tableRow.setupRow(widgetItems.size(), /*resizeDelayMs=*/ 0);
             tableRow.setGravity(Gravity.TOP);
             for (WidgetItem widgetItem : widgetItems) {
                 WidgetCell widgetCell = addItemCell(tableRow);
@@ -121,9 +121,10 @@
         setVisibility(VISIBLE);
     }
 
-    private WidgetCell addItemCell(ViewGroup parent) {
+    private WidgetCell addItemCell(WidgetTableRow parent) {
         WidgetCell widget = (WidgetCell) LayoutInflater.from(
                 getContext()).inflate(R.layout.widget_cell, parent, false);
+        widget.addPreviewReadyListener(parent);
         widget.setOnClickListener(mWidgetCellOnClickListener);
 
         View previewContainer = widget.findViewById(R.id.widget_preview_container);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index f835e18..5d71db6 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -67,7 +67,7 @@
 
     // This ratio defines the max percentage of content area that the recommendations can display
     // with respect to the bottom sheet's height.
-    private static final float RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE = 0.60f;
+    private static final float RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE = 0.70f;
     private FrameLayout mSuggestedWidgetsContainer;
     private WidgetsListHeader mSuggestedWidgetsHeader;
     private PackageUserKey mSuggestedWidgetsPackageUserKey;
diff --git a/src/com/android/launcher3/widget/util/WidgetDragScaleUtils.java b/src/com/android/launcher3/widget/util/WidgetDragScaleUtils.java
new file mode 100644
index 0000000..b8e7248
--- /dev/null
+++ b/src/com/android/launcher3/widget/util/WidgetDragScaleUtils.java
@@ -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.widget.util;
+
+import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx;
+
+import android.content.Context;
+import android.util.Size;
+
+import androidx.annotation.Px;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.ItemInfo;
+
+/** Utility classes to evaluate widget scale during drag and drops. **/
+public final class WidgetDragScaleUtils {
+    // Widgets are 5% scaled down relative to their size to have shadow display well inside the
+    // drop target frame (if its possible to scale it down within visible area under the finger).
+    private static final float WIDGET_SCALE_DOWN = 0.05f;
+
+    /**
+     * Returns the scale to be applied to given dragged view to scale it down relative to the
+     * spring loaded workspace. Applies additional scale down offset to get it a little inside
+     * the drop target frame. If the relative scale is smaller than minimum size needed to keep the
+     * view visible under the finger, scale down is performed only until the minimum size.
+     */
+    @Px
+    public static float getWidgetDragScalePx(Context context, DeviceProfile deviceProfile,
+            @Px float draggedViewWidthPx, @Px float draggedViewHeightPx, ItemInfo itemInfo) {
+        int minSize = context.getResources().getDimensionPixelSize(
+                R.dimen.widget_drag_view_min_scale_down_size);
+        Size widgetSizesPx = getWidgetSizePx(deviceProfile, itemInfo.spanX, itemInfo.spanY);
+
+        // We add workspace spring load scale, since the widget's drop target is also scaled, so
+        // the widget size is essentially that smaller.
+        float desiredWidgetScale = deviceProfile.getWorkspaceSpringLoadScale(context)
+                - WIDGET_SCALE_DOWN;
+        float desiredWidgetWidthPx = Math.max(minSize,
+                (desiredWidgetScale * widgetSizesPx.getWidth()));
+        float desiredWidgetHeightPx = Math.max(minSize,
+                desiredWidgetScale * widgetSizesPx.getHeight());
+
+        final float bitmapAspectRatio = draggedViewWidthPx / draggedViewHeightPx;
+        final float containerAspectRatio = desiredWidgetWidthPx / desiredWidgetHeightPx;
+
+        // This downscales large views to fit inside drop target frame. Smaller drawable views may
+        // be up-scaled if they are smaller than the min size;
+        final float scale = bitmapAspectRatio >= containerAspectRatio ? desiredWidgetWidthPx
+                / draggedViewWidthPx : desiredWidgetHeightPx / draggedViewHeightPx;
+        // scale in terms of dp to be applied to the drag shadow during drag and drop
+        return (draggedViewWidthPx * scale) - draggedViewWidthPx;
+    }
+}
diff --git a/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java
index b62dbd1..9865516 100644
--- a/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -53,6 +53,11 @@
     }
 
     @Override
+    public int getTitle() {
+        return R.string.all_apps_label;
+    }
+
+    @Override
     public int getVisibleElements(Launcher launcher) {
         return ALL_APPS_CONTENT;
     }
diff --git a/tests/Android.bp b/tests/Android.bp
index a8fba85..1dcb2a6 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -198,6 +198,9 @@
         "androidx.test.uiautomator_uiautomator",
         "androidx.core_core-animation-testing",
         "androidx.test.ext.junit",
+        "androidx.test.espresso.core",
+        "androidx.test.espresso.contrib",
+        "androidx.test.espresso.intents",
         "androidx.test.rules",
         "uiautomator-helpers",
         "inline-mockito-robolectric-prebuilt",
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index c3b7a2a..59d0de6 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -169,13 +169,11 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-    public static final String TWO_NEXUS_LAUNCHER_ACTIVITY_WHILE_UNLOCKING = "b/273347463";
     public static final String ICON_MISSING = "b/282963545";
     public static final String UIOBJECT_STALE_ELEMENT = "b/319501259";
     public static final String TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE = "b/326908466";
     public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890";
     public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
-    public static final String CLOCK_ICON_DRAWABLE_LEAKING = "b/319168409";
 
     public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
     public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
@@ -183,6 +181,9 @@
     public static final String REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED =
             "unstash-bubble-bar-if-stashed";
 
+    public static final String REQUEST_INJECT_FAKE_TRACKPAD = "inject-fake-trackpad";
+    public static final String REQUEST_EJECT_FAKE_TRACKPAD = "eject-fake-trackpad";
+
     /** Logs {@link Log#d(String, String)} if {@link #sDebugTracing} is true. */
     public static void testLogD(String tag, String message) {
         if (!sDebugTracing) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index e378733..3405635 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -57,7 +57,7 @@
  *
  * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
  */
-@AllowedDevices(allowed = [DeviceProduct.CF_PHONE])
+@AllowedDevices(allowed = [DeviceProduct.CF_PHONE, DeviceProduct.HOST_SIDE_X86_64])
 @IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
 abstract class AbstractDeviceProfileTest {
     protected val testContext: Context = InstrumentationRegistry.getInstrumentation().context
diff --git a/tests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt
similarity index 91%
rename from tests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt
index 7ff544d..5344d5c 100644
--- a/tests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt
@@ -25,7 +25,7 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.verifyNoMoreInteractions
 import org.mockito.kotlin.whenever
 
 /** Test for AbstractFloatingViewHelper */
@@ -60,7 +60,8 @@
             AbstractFloatingView.TYPE_ALL
         )
 
-        verifyZeroInteractions(view)
+        // b/343530737
+        verifyNoMoreInteractions(view)
         verify(folderView).close(true)
         verify(taskMenuView).close(true)
     }
@@ -73,7 +74,8 @@
             AbstractFloatingView.TYPE_TASK_MENU
         )
 
-        verifyZeroInteractions(view)
+        // b/343530737
+        verifyNoMoreInteractions(view)
         verify(folderView, never()).close(any())
         verify(taskMenuView).close(true)
     }
@@ -86,7 +88,8 @@
             AbstractFloatingView.TYPE_PIN_IME_POPUP
         )
 
-        verifyZeroInteractions(view)
+        // b/343530737
+        verifyNoMoreInteractions(view)
         verify(folderView, never()).close(any())
         verify(taskMenuView, never()).close(any())
     }
@@ -99,7 +102,8 @@
             AbstractFloatingView.TYPE_FOLDER or AbstractFloatingView.TYPE_TASK_MENU
         )
 
-        verifyZeroInteractions(view)
+        // b/343530737
+        verifyNoMoreInteractions(view)
         verify(folderView).close(false)
         verify(taskMenuView).close(false)
     }
diff --git a/tests/src/com/android/launcher3/settings/SettingsActivityTest.java b/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/settings/SettingsActivityTest.java
rename to tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java
diff --git a/tests/src/com/android/launcher3/util/LockedUserStateTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/util/LockedUserStateTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt
index 2c4a54f..2711d7a 100644
--- a/tests/src/com/android/launcher3/util/LockedUserStateTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt
@@ -28,7 +28,7 @@
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.verifyNoMoreInteractions
 import org.mockito.kotlin.whenever
 
 /** Unit tests for {@link LockedUserState} */
@@ -58,7 +58,8 @@
         val action: Runnable = mock()
         val state = LockedUserState(context)
         state.runOnUserUnlocked(action)
-        verifyZeroInteractions(action)
+        // b/343530737
+        verifyNoMoreInteractions(action)
         state.mUserUnlockedReceiver.onReceive(context, Intent(Intent.ACTION_USER_UNLOCKED))
         verify(action).run()
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/util/WidgetDragScaleUtilsTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/util/WidgetDragScaleUtilsTest.kt
new file mode 100644
index 0000000..63833e4
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/util/WidgetDragScaleUtilsTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.widget.util
+
+import android.content.Context
+import android.graphics.Point
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.R
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetDragScaleUtilsTest {
+    private lateinit var context: Context
+    private lateinit var itemInfo: ItemInfo
+    private lateinit var deviceProfile: DeviceProfile
+
+    @Before
+    fun setup() {
+        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+
+        itemInfo = ItemInfo()
+
+        deviceProfile =
+            Mockito.spy(LauncherAppState.getIDP(context).getDeviceProfile(context).copy(context))
+
+        doReturn(0.8f)
+                .whenever(deviceProfile).getWorkspaceSpringLoadScale(any(Context::class.java))
+        deviceProfile.cellLayoutBorderSpacePx = Point(CELL_SPACING, CELL_SPACING)
+        deviceProfile.widgetPadding.setEmpty()
+    }
+
+    @Test
+    fun getWidgetDragScalePx_largeDraggedView_downScaled() {
+        val minSize = context.resources.getDimensionPixelSize(
+                R.dimen.widget_drag_view_min_scale_down_size)
+        whenever(deviceProfile.cellSize).thenReturn(Point(minSize * 2, minSize * 2))
+
+        itemInfo.spanX = 2
+        itemInfo.spanY = 2
+
+        val widgetSize = WidgetSizes.getWidgetSizePx(deviceProfile, itemInfo.spanX, itemInfo.spanY)
+        // Assume dragged view was a drawable which was larger than widget's size.
+        val draggedViewWidthPx = widgetSize.width + 0.5f * widgetSize.width
+        val draggedViewHeightPx = widgetSize.height + 0.5f * widgetSize.height
+        // Returns negative scale pixels - i.e. downscaled
+        assertThat(
+                WidgetDragScaleUtils.getWidgetDragScalePx(
+                    context,
+                    deviceProfile,
+                    draggedViewWidthPx,
+                    draggedViewHeightPx,
+                    itemInfo
+                )
+            )
+            .isLessThan(0)
+    }
+
+    @Test
+    fun getWidgetDragScalePx_draggedViewSameAsWidgetSize_downScaled() {
+        val minSize = context.resources.getDimensionPixelSize(
+                R.dimen.widget_drag_view_min_scale_down_size)
+        whenever(deviceProfile.cellSize).thenReturn(Point(minSize * 2, minSize * 2))
+        itemInfo.spanX = 4
+        itemInfo.spanY = 2
+
+        val widgetSize = WidgetSizes.getWidgetSizePx(deviceProfile, itemInfo.spanX, itemInfo.spanY)
+        // Assume dragged view was a drawable which was larger than widget's size.
+        val draggedViewWidthPx = widgetSize.width.toFloat()
+        val draggedViewHeightPx = widgetSize.height.toFloat()
+        // Returns negative scale pixels - i.e. downscaled
+        // Even if dragged view was of same size as widget's drop target, to accommodate the spring
+        // load scaling of workspace and additionally getting the view inside of drop target frame,
+        // widget would be downscaled.
+        assertThat(
+                WidgetDragScaleUtils.getWidgetDragScalePx(
+                    context,
+                    deviceProfile,
+                    draggedViewWidthPx,
+                    draggedViewHeightPx,
+                    itemInfo
+                )
+            )
+            .isLessThan(0)
+    }
+
+    @Test
+    fun getWidgetDragScalePx_draggedViewSmallerThanMinSize_scaledSizeIsAtLeastMinSize() {
+        val minSizePx =
+            context.resources.getDimensionPixelSize(R.dimen.widget_drag_view_min_scale_down_size)
+        // Assume min size is greater than cell size, so that, we know the upscale of dragged view
+        // is due to min size enforcement.
+        whenever(deviceProfile.cellSize).thenReturn(Point(minSizePx / 2, minSizePx / 2))
+        itemInfo.spanX = 1
+        itemInfo.spanY = 1
+
+        val draggedViewWidthPx = minSizePx - 15f
+        val draggedViewHeightPx = minSizePx - 15f
+
+        // Returns positive scale pixels - i.e. up-scaled
+        val finalScalePx =
+            WidgetDragScaleUtils.getWidgetDragScalePx(
+                context,
+                deviceProfile,
+                draggedViewWidthPx,
+                draggedViewHeightPx,
+                itemInfo
+            )
+
+        val effectiveWidthPx = draggedViewWidthPx + finalScalePx
+        val scaleFactor = (draggedViewWidthPx + finalScalePx) / draggedViewWidthPx
+        val effectiveHeightPx = scaleFactor * draggedViewHeightPx
+        // Both original height and width were smaller than min size, scaling them down below min
+        // size would have made them not visible under the finger. Here, as expected, widget is
+        // at least as large as min size.
+        assertThat(effectiveWidthPx).isAtLeast(minSizePx)
+        assertThat(effectiveHeightPx).isAtLeast(minSizePx)
+    }
+
+    companion object {
+        const val CELL_SPACING = 10
+    }
+}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
index 4cd2a07..6ce1cb7 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
@@ -130,7 +130,7 @@
     public void lockPrivateProfile_requestsQuietModeAsTrue() throws Exception {
         when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(false);
 
-        mPrivateProfileManager.lockPrivateProfile();
+        mPrivateProfileManager.setQuietMode(true /* lock */);
 
         awaitTasksCompleted();
         Mockito.verify(mUserManager).requestQuietModeEnabled(true, PRIVATE_HANDLE);
@@ -140,7 +140,7 @@
     public void unlockPrivateProfile_requestsQuietModeAsFalse() throws Exception {
         when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(true);
 
-        mPrivateProfileManager.unlockPrivateProfile();
+        mPrivateProfileManager.setQuietMode(false /* unlock */);
 
         awaitTasksCompleted();
         Mockito.verify(mUserManager).requestQuietModeEnabled(false, PRIVATE_HANDLE);
@@ -176,7 +176,7 @@
         doNothing().when(privateProfileManager).expandPrivateSpace();
         when(privateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED);
 
-        privateProfileManager.unlockPrivateProfile();
+        privateProfileManager.setQuietMode(false /* unlock */);
         privateProfileManager.reset();
 
         awaitTasksCompleted();
@@ -194,7 +194,7 @@
         doNothing().when(privateProfileManager).expandPrivateSpace();
         when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
 
-        privateProfileManager.lockPrivateProfile();
+        privateProfileManager.setQuietMode(true /* lock */);
         privateProfileManager.reset();
 
         awaitTasksCompleted();
@@ -208,7 +208,7 @@
         ArgumentCaptor<Intent> acIntent = ArgumentCaptor.forClass(Intent.class);
         mPrivateProfileManager.setPrivateSpaceSettingsAvailable(true);
 
-        mPrivateProfileManager.openPrivateSpaceSettings(null);
+        mContext.startActivity(expectedIntent);
 
         Mockito.verify(mContext).startActivity(acIntent.capture());
         assertEquals("Intent Action is different",
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
index 512b2ac..eac7f63 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
@@ -133,7 +133,7 @@
         Bitmap unlockButton = getBitmap(mContext.getDrawable(R.drawable.ic_lock));
         PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager);
         when(privateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED);
-        privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+        privateProfileManager.bindPrivateSpaceHeaderViewElements(mPsHeaderLayout);
         awaitTasksCompleted();
 
         int totalContainerHeaderView = 0;
@@ -168,7 +168,7 @@
         PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager);
         when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
         when(privateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(true);
-        privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+        privateProfileManager.bindPrivateSpaceHeaderViewElements(mPsHeaderLayout);
         awaitTasksCompleted();
 
         int totalContainerHeaderView = 0;
@@ -210,7 +210,7 @@
         PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager);
         when(privateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
         when(privateProfileManager.isPrivateSpaceSettingsAvailable()).thenReturn(false);
-        privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+        privateProfileManager.bindPrivateSpaceHeaderViewElements(mPsHeaderLayout);
         awaitTasksCompleted();
 
         int totalContainerHeaderView = 0;
@@ -248,7 +248,7 @@
         Bitmap transitionImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_transition_image));
         PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager);
         when(privateProfileManager.getCurrentState()).thenReturn(STATE_TRANSITION);
-        privateProfileManager.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+        privateProfileManager.bindPrivateSpaceHeaderViewElements(mPsHeaderLayout);
         awaitTasksCompleted();
 
         int totalContainerHeaderView = 0;
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
new file mode 100644
index 0000000..9537e1c
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.allapps;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PRIVATESPACE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.util.ActivityContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class PrivateSpaceSettingsButtonTest {
+
+    private PrivateSpaceSettingsButton mVut;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Context context = new ActivityContextWrapper(getApplicationContext());
+        mVut = new PrivateSpaceSettingsButton(context);
+    }
+
+    @Test
+    public void privateSpaceSettingsAppInfo_hasCorrectIdAndContainer() {
+        AppInfo appInfo = mVut.createPrivateSpaceSettingsAppInfo();
+
+        assertThat(appInfo.id).isEqualTo(CONTAINER_PRIVATESPACE);
+        assertThat(appInfo.container).isEqualTo(CONTAINER_PRIVATESPACE);
+    }
+}
diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
index f3f6fa5..05a1224 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
@@ -120,7 +120,6 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
-    @ScreenRecordRule.ScreenRecord // b/322228038
     public void testAllAppsFromHome() {
         // Test opening all apps
         assertNotNull("switchToAllApps() returned null",
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 1c41ded..41abcf8 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.Test;
 
@@ -194,6 +195,7 @@
     @PlatinumTest(focusArea = "launcher")
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/343953783
     public void testDragAppIcon() {
 
         final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
@@ -221,24 +223,7 @@
     public void testDragAppIconToMultipleWorkspaceCells() throws Exception {
         long startTime, endTime, elapsedTime;
         Point[] targets = TestUtil.getCornersAndCenterPositions(mLauncher);
-
-        for (Point target : targets) {
-            startTime = SystemClock.uptimeMillis();
-            final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-            allApps.freeze();
-            try {
-                allApps.getAppIcon(TEST_APP_NAME).dragToWorkspace(target.x, target.y);
-            } finally {
-                allApps.unfreeze();
-            }
-            // Reset the workspace for the next shortcut creation.
-            reinitializeLauncherData(true);
-            endTime = SystemClock.uptimeMillis();
-            elapsedTime = endTime - startTime;
-            Log.d("testDragAppIconToWorkspaceCellTime",
-                    "Milliseconds taken to drag app icon to workspace cell: " + elapsedTime);
-        }
-
+        reinitializeLauncherData(true);
         // test to move a shortcut to other cell.
         final HomeAppIcon launcherTestAppIcon = createShortcutInCenterIfNotExist(TEST_APP_NAME);
         for (Point target : targets) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
similarity index 100%
rename from tests/multivalentTests/src/com/android/launcher3/folder/FolderNameProviderTest.java
rename to tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
index 78c61d5..370af0c 100644
--- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -35,7 +35,7 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.same
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.verifyNoMoreInteractions
 import org.mockito.kotlin.whenever
 
 /** Tests for [AddWorkspaceItemsTask] */
@@ -97,7 +97,8 @@
         val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd)
 
         assertThat(addedItems.size).isEqualTo(0)
-        verifyZeroInteractions(mWorkspaceItemSpaceFinder)
+        // b/343530737
+        verifyNoMoreInteractions(mWorkspaceItemSpaceFinder)
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 733f1e9..b3675a6 100644
--- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -38,7 +38,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.backup.BackupManager;
@@ -243,7 +243,8 @@
         // Then
         assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds);
         assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse();
-        verifyZeroInteractions(mMockController);
+        // b/343530737
+        verifyNoMoreInteractions(mMockController);
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index a672c01..e92d641 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -113,6 +113,8 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/329935119
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/329935119
     public void testSinglePageDragIconWhenMultiplePageScrollingIsPossible() {
         Workspace workspace = mLauncher.getWorkspace();
 
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 2e3944d..e10893e 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -359,21 +359,6 @@
     }
 
     /**
-     * Gets Overview Actions specific to grouped tasks.
-     *
-     * @return The Overview group actions bar
-     */
-    @NonNull
-    public OverviewActions getOverviewGroupActions() {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to get overview group actions")) {
-            verifyActiveContainer();
-            UiObject2 groupActions = mLauncher.waitForOverviewObject("group_action_buttons");
-            return new OverviewActions(groupActions, mLauncher);
-        }
-    }
-
-    /**
      * Returns if clear all button is visible.
      */
     public boolean isClearAllVisible() {
@@ -469,13 +454,13 @@
 
             if (isActionsViewVisible()) {
                 if (task.isTaskSplit()) {
-                    mLauncher.waitForOverviewObject("group_action_buttons");
+                    mLauncher.waitForOverviewObject("action_save_app_pair");
                 } else {
                     mLauncher.waitForOverviewObject("action_buttons");
                 }
             } else {
                 mLauncher.waitUntilOverviewObjectGone("action_buttons");
-                mLauncher.waitUntilOverviewObjectGone("group_action_buttons");
+                mLauncher.waitUntilOverviewObjectGone("action_save_app_pair");
             }
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 68b0a36..f02a0c2 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -711,7 +711,7 @@
             final LogEventChecker eventChecker = mEventChecker;
             mEventChecker = null;
             if (checkEvents) {
-                final String eventMismatch = eventChecker.verify(0, false);
+                final String eventMismatch = eventChecker.verify(0);
                 if (eventMismatch != null) {
                     message = message + ";\n" + eventMismatch;
                 }
@@ -2318,6 +2318,14 @@
         getTestInfo(TestProtocol.REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED);
     }
 
+    public void injectFakeTrackpad() {
+        getTestInfo(TestProtocol.REQUEST_INJECT_FAKE_TRACKPAD);
+    }
+
+    public void ejectFakeTrackpad() {
+        getTestInfo(TestProtocol.REQUEST_EJECT_FAKE_TRACKPAD);
+    }
+
     /** Blocks the taskbar from automatically stashing based on time. */
     public void enableBlockTimeout(boolean enable) {
         getTestInfo(enable
@@ -2408,7 +2416,7 @@
             if (mEventChecker != null) {
                 mEventChecker = null;
                 if (mCheckEventsForSuccessfulGestures) {
-                    final String message = eventChecker.verify(WAIT_TIME_MS, true);
+                    final String message = eventChecker.verify(WAIT_TIME_MS);
                     if (message != null) {
                         dumpDiagnostics(message);
                         checkForAnomaly();
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 672c6e0..055a357 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -15,10 +15,6 @@
  */
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.testing.shared.TestProtocol.SEQUENCE_MAIN;
-import static com.android.launcher3.testing.shared.TestProtocol.SEQUENCE_PILFER;
-import static com.android.launcher3.testing.shared.TestProtocol.SEQUENCE_TIS;
-
 import android.os.SystemClock;
 
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -87,25 +83,11 @@
         mLauncher.getTestInfo(TestProtocol.REQUEST_STOP_EVENT_LOGGING);
     }
 
-    String verify(long waitForExpectedCountMs, boolean successfulGesture) {
+    String verify(long waitForExpectedCountMs) {
         final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs);
         if (actualEvents == null) return "null event sequences because launcher likely died";
 
-        final String lowLevelDiags = lowLevelMismatchDiagnostics(actualEvents);
-        // If we have a sequence mismatch for a successful gesture, we want to provide all low-level
-        // details.
-        if (successfulGesture) {
-            return lowLevelDiags;
-        }
-
-        final String sequenceMismatchInEnglish = highLevelMismatchDiagnostics(actualEvents);
-
-        if (sequenceMismatchInEnglish != null) {
-            LauncherInstrumentation.log(lowLevelDiags);
-            return "Hint: " + sequenceMismatchInEnglish;
-        } else {
-            return lowLevelDiags;
-        }
+        return lowLevelMismatchDiagnostics(actualEvents);
     }
 
     private String lowLevelMismatchDiagnostics(ListMap<String> actualEvents) {
@@ -140,42 +122,6 @@
         return hasMismatches ? "Mismatching events: " + sb.toString() : null;
     }
 
-    private String highLevelMismatchDiagnostics(ListMap<String> actualEvents) {
-        if (!mExpectedEvents.getNonNull(SEQUENCE_TIS).isEmpty()
-                && actualEvents.getNonNull(SEQUENCE_TIS).isEmpty()) {
-            return "TouchInteractionService didn't receive any of the touch events sent by the "
-                    + "test";
-        }
-        if (getMismatchPosition(mExpectedEvents.getNonNull(SEQUENCE_TIS),
-                actualEvents.getNonNull(SEQUENCE_TIS)) != -1) {
-            // If TIS has a mismatch that we can't convert to high-level diags, don't convert
-            // other sequences either.
-            return null;
-        }
-
-        if (mExpectedEvents.getNonNull(SEQUENCE_PILFER).size() == 1
-                && actualEvents.getNonNull(SEQUENCE_PILFER).isEmpty()) {
-            return "Launcher didn't detect the navigation gesture sent by the test";
-        }
-        if (mExpectedEvents.getNonNull(SEQUENCE_PILFER).isEmpty()
-                && actualEvents.getNonNull(SEQUENCE_PILFER).size() == 1) {
-            return "Launcher detected a navigation gesture, but the test didn't send one";
-        }
-        if (getMismatchPosition(mExpectedEvents.getNonNull(SEQUENCE_PILFER),
-                actualEvents.getNonNull(SEQUENCE_PILFER)) != -1) {
-            // If Pilfer has a mismatch that we can't convert to high-level diags, don't analyze
-            // other sequences.
-            return null;
-        }
-
-        if (!mExpectedEvents.getNonNull(SEQUENCE_MAIN).isEmpty()
-                && actualEvents.getNonNull(SEQUENCE_MAIN).isEmpty()) {
-            return "None of the touch or keyboard events sent by the test was received by "
-                    + "Launcher's main thread";
-        }
-        return null;
-    }
-
     // If the list of actual events matches the list of expected events, returns -1, otherwise
     // the position of the mismatch.
     private static int getMismatchPosition(List<Pattern> expected, List<String> actual) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
index 486a63b..d7c40a0 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -17,7 +17,6 @@
 package com.android.launcher3.tapl;
 
 import androidx.annotation.NonNull;
-import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
 
 /**
@@ -111,12 +110,4 @@
             }
         }
     }
-
-    /** Asserts that an item matching the given string is present in the overview actions. */
-    public void assertHasAction(String text) {
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "want to check if the action [" + text + "] is present")) {
-            mLauncher.waitForObjectInContainer(mOverviewActions, By.text(text));
-        }
-    }
 }