Merge "Update test infra to allow split placeholder view in all apps" into main
diff --git a/Android.bp b/Android.bp
index eca0778..61042f6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -108,6 +108,7 @@
     ],
     srcs: [
         "tests/multivalentTests/tapl/**/*.java",
+        "tests/multivalentTests/tapl/**/*.kt",
     ],
     resource_dirs: [],
     manifest: "tests/multivalentTests/tapl/AndroidManifest.xml",
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 7032d5f..82ae4cb 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -92,6 +92,13 @@
 }
 
 flag {
+    name: "enable_predictive_back_gesture"
+    namespace: "launcher"
+    description: "Enable predictive back gesture on Launcher (including all apps and widget picker)."
+    bug: "238475505"
+}
+
+flag {
     name: "enable_shortcut_dont_suggest_app"
     namespace: "launcher"
     description: "Enables don't suggest app shortcut for suggested apps"
@@ -147,3 +154,10 @@
   description: "Enables widget suggestions in widget picker to be displayed in categories"
   bug: "318410881"
 }
+
+flag {
+    name: "use_activity_overlay"
+    namespace: "launcher"
+    description: "Use an activity for home screen overlay"
+    bug: "273828110"
+}
diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index b846604..4e16e7f 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -34,3 +34,10 @@
     description: "This flag disables accessibility drag for Private Space Apps."
     bug: "289223923"
 }
+
+flag {
+    name: "private_space_restrict_item_drag"
+    namespace: "launcher_search"
+    description: "This flag disables drag and drop for Private Space Items."
+    bug: "289223923"
+}
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 823a86e..9d599c9 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -19,6 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/task"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="false"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 60827cd..3cafcfd 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -19,6 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/task"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="true"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index d20afd3..e91e773 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -24,6 +24,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/task"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:clipChildren="false"
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 8a3ffb5..e45d9fd 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -53,5 +53,5 @@
     <string name="setup_wizard_pkg" translatable="false" />
 
     <!-- This is a float because it is converted to dp later in DeviceProfile -->
-    <item name="taskbar_icon_size" type="dimen" format="float">48.4</item>
+    <item name="taskbar_icon_size" type="dimen" format="float">44</item>
 </resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index b23c283..325c255 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -267,7 +267,7 @@
     <string name="taskbar_edu_features">Do more with the Taskbar</string>
     <!-- Title in dialog that shows a user how to pin the Taskbar. [CHAR_LIMIT 60] -->
     <string name="taskbar_edu_pinning_title">Always show the Taskbar</string>
-    <!-- Text in dialog that shows a user how to pin the Taskbar. [CHAR_LIMIT 60] -->
+    <!-- Text in dialog that shows a user how to pin the Taskbar. [CHAR_LIMIT 150] -->
     <string name="taskbar_edu_pinning_standalone">To always show the Taskbar on the bottom of your screen, touch &amp; hold the divider</string>
     <!-- Text on button to exit a tutorial [CHAR_LIMIT=16] -->
     <string name="taskbar_edu_close">Close</string>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index e8a3d3e..05e1535 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1677,7 +1677,7 @@
                 }
             };
 
-            if (fromPredictiveBack) {
+            if (fromPredictiveBack && rectFSpringAnim != null) {
                 rectFSpringAnim.addAnimatorListener(endListener);
             } else {
                 anim.addListener(endListener);
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index b9b4461..436fe3b 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -28,6 +28,7 @@
 import android.content.ClipDescription;
 import android.content.Intent;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.View;
 import android.view.WindowInsetsController;
 import android.view.WindowManager;
@@ -35,6 +36,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.dragndrop.SimpleDragLayer;
+import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.widget.BaseWidgetSheet;
@@ -43,9 +45,13 @@
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.ArrayList;
+import java.util.Locale;
 
 /** An Activity that can host Launcher's widget picker. */
 public class WidgetPickerActivity extends BaseActivity {
+    private static final String TAG = "WidgetPickerActivity";
+    private static final boolean DEBUG = false;
+
     /**
      * Name of the extra that indicates that a widget being dragged.
      *
@@ -54,10 +60,19 @@
      */
     private static final String EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag";
 
+    // Intent extras that specify the desired widget width and height. If these are not specified in
+    // the intent, then widgets will not be filtered for size.
+    private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
+    private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
+
+
     private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
     private WidgetsModel mModel;
     private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
 
+    private int mDesiredWidgetWidth;
+    private int mDesiredWidgetHeight;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -82,6 +97,13 @@
         widgetSheet.disableNavBarScrim(true);
         widgetSheet.addOnCloseListener(this::finish);
 
+        // A value of 0 for either size means that no filtering will occur in that dimension. If
+        // both values are 0, then no size filtering will occur.
+        mDesiredWidgetWidth =
+                getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_WIDTH, 0);
+        mDesiredWidgetHeight =
+                getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
+
         refreshAndBindWidgets();
     }
 
@@ -160,9 +182,108 @@
             final ArrayList<WidgetsListBaseEntry> widgets =
                     mModel.getFilteredWidgetsListForPicker(
                             app.getContext(),
-                            /*widgetItemFilter=*/ item -> item.widgetInfo != null
+                            /*widgetItemFilter=*/ widget -> {
+                                final WidgetAcceptabilityVerdict verdict =
+                                        isWidgetAcceptable(widget);
+                                verdict.maybeLogVerdict();
+                                return verdict.isAcceptable;
+                            }
                     );
             MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
         });
     }
+
+    private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
+        final AppWidgetProviderInfo info = widget.widgetInfo;
+        if (info == null) {
+            return rejectWidget(widget, "shortcut");
+        }
+
+        if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
+            // Accept the widget if the desired dimensions are unspecified.
+            return acceptWidget(widget);
+        }
+
+        final boolean isHorizontallyResizable =
+                (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
+        if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
+            if (info.maxResizeWidth > 0 && info.maxResizeWidth < mDesiredWidgetWidth) {
+                return rejectWidget(
+                        widget,
+                        String.format(
+                                Locale.ENGLISH,
+                                "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
+                                info.maxResizeWidth,
+                                mDesiredWidgetWidth));
+            }
+
+            final int minWidth = info.minResizeWidth > 0 ? info.minResizeWidth : info.minWidth;
+            if (minWidth > mDesiredWidgetWidth) {
+                return rejectWidget(
+                        widget,
+                        String.format(
+                                Locale.ENGLISH,
+                                "minWidth[%d] > mDesiredWidgetWidth[%d]",
+                                minWidth,
+                                mDesiredWidgetWidth));
+            }
+        }
+
+        final boolean isVerticallyResizable =
+                (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
+        if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
+            if (info.maxResizeHeight > 0 && info.maxResizeHeight < mDesiredWidgetHeight) {
+                return rejectWidget(
+                        widget,
+                        String.format(
+                                Locale.ENGLISH,
+                                "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
+                                info.maxResizeHeight,
+                                mDesiredWidgetHeight));
+            }
+
+            final int minHeight = info.minResizeHeight > 0 ? info.minResizeHeight : info.minHeight;
+            if (minHeight > mDesiredWidgetHeight) {
+                return rejectWidget(
+                        widget,
+                        String.format(
+                                Locale.ENGLISH,
+                                "minHeight[%d] > mDesiredWidgetHeight[%d]",
+                                minHeight,
+                                mDesiredWidgetHeight));
+            }
+        }
+
+        if (!isHorizontallyResizable
+                && !isVerticallyResizable
+                && (info.minWidth < mDesiredWidgetWidth || info.minHeight < mDesiredWidgetHeight)) {
+            return rejectWidget(widget, "too small and not resizeable");
+        }
+
+        return acceptWidget(widget);
+    }
+
+    private static WidgetAcceptabilityVerdict rejectWidget(
+            WidgetItem widget, String rejectionReason) {
+        return new WidgetAcceptabilityVerdict(false, widget.label, rejectionReason);
+    }
+
+    private static WidgetAcceptabilityVerdict acceptWidget(WidgetItem widget) {
+        return new WidgetAcceptabilityVerdict(true, widget.label, "");
+    }
+
+    private record WidgetAcceptabilityVerdict(
+            boolean isAcceptable, String widgetLabel, String reason) {
+        void maybeLogVerdict() {
+            // Only log a verdict if a reason is specified.
+            if (DEBUG && !reason.isEmpty()) {
+                Log.i(TAG, String.format(
+                        Locale.ENGLISH,
+                        "%s: %s because %s",
+                        widgetLabel,
+                        isAcceptable ? "accepted" : "rejected",
+                        reason));
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 6160378..f4cbf17 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -15,9 +15,11 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 
 import android.app.prediction.AppTarget;
+import android.content.Context;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -29,6 +31,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -93,9 +96,21 @@
             servicePredictedItems.addAll(localFilteredWidgets);
         }
 
-        List<ItemInfo> items = servicePredictedItems.stream()
-                .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION))
-                .collect(Collectors.toList());
+        List<ItemInfo> items;
+        if (enableCategorizedWidgetSuggestions()) {
+            Context context = appState.getContext();
+            WidgetRecommendationCategoryProvider categoryProvider =
+                    WidgetRecommendationCategoryProvider.newInstance(context);
+            items = servicePredictedItems.stream()
+                    .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
+                            categoryProvider.getWidgetRecommendationCategory(context, it)))
+                    .collect(Collectors.toList());
+        } else {
+            items = servicePredictedItems.stream()
+                    .map(it -> new PendingAddWidgetInfo(it.widgetInfo,
+                            CONTAINER_WIDGETS_PREDICTION)).collect(
+                            Collectors.toList());
+        }
         FixedContainerItems fixedContainerItems =
                 new FixedContainerItems(mPredictorState.containerId, items);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 5caf004..f15d12b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
@@ -47,7 +48,8 @@
 public final class KeyboardQuickSwitchController implements
         TaskbarControllers.LoggableTaskbarController {
 
-    static final int MAX_TASKS = 6;
+    @VisibleForTesting
+    public static final int MAX_TASKS = 6;
 
     @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 9b8ab33..2f11fd7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -780,9 +780,11 @@
             handleSetupUi();
 
             // Hide back button in SUW if keyboard is showing (IME draws its own back).
-            mPropertyHolders.add(new StatePropertyHolder(
-                    mBackButtonAlpha.get(ALPHA_INDEX_SUW),
-                    flags -> (flags & FLAG_IME_VISIBLE) == 0));
+            if (mIsImeRenderingNavButtons) {
+                mPropertyHolders.add(new StatePropertyHolder(
+                        mBackButtonAlpha.get(ALPHA_INDEX_SUW),
+                        flags -> (flags & FLAG_IME_VISIBLE) == 0));
+            }
         } else if (isInKidsMode) {
             int iconSize = res.getDimensionPixelSize(
                     R.dimen.taskbar_icon_size_kids);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index c883759..db7d0eb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -60,8 +60,10 @@
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SystemUiProxy;
 
 import java.io.PrintWriter;
@@ -951,6 +953,15 @@
         if (mActivity.isHardwareKeyboard() && DisplayController.isPinnedTaskbar(mActivity)) {
             return false;
         }
+
+        // Do not stash if hardware keyboard is attached, in 3 button nav and desktop windowing mode
+        DesktopVisibilityController visibilityController =
+                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+        if (visibilityController != null && mActivity.isHardwareKeyboard()
+                && mActivity.isThreeButtonNav() && visibilityController.areFreeformTasksVisible()) {
+            return false;
+        }
+
         return mIsImeShowing || mIsImeSwitcherShowing;
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 964d329..5424fcf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar.allapps;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.Flags.enablePredictiveBackGesture;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -168,7 +169,7 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mActivityContext.addOnDeviceProfileChangeListener(this);
-        if (FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get()) {
+        if (enablePredictiveBackGesture()) {
             mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(mViewOutlineProvider);
             mAppsView.getAppsRecyclerViewContainer().setClipToOutline(true);
             OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
@@ -183,7 +184,7 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mActivityContext.removeOnDeviceProfileChangeListener(this);
-        if (FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get()) {
+        if (enablePredictiveBackGesture()) {
             mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(null);
             mAppsView.getAppsRecyclerViewContainer().setClipToOutline(false);
             OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 5819bb3..6dc7db7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.quickstep.SystemUiProxy;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.bubbles.IBubblesListener;
 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.common.bubbles.BubbleInfo;
@@ -101,8 +102,8 @@
      *
      * @see #onTaskbarRecreated()
      */
-    private static boolean sBubbleBarEnabled =
-            SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
+    private static boolean sBubbleBarEnabled = Flags.enableBubbleBar()
+            || SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
 
     /** Whether showing bubbles in the launcher bubble bar is enabled. */
     public static boolean isBubbleBarEnabled() {
@@ -111,8 +112,10 @@
 
     /** Re-reads the value of the flag from SystemProperties when taskbar is recreated. */
     public static void onTaskbarRecreated() {
-        sBubbleBarEnabled = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
+        sBubbleBarEnabled = Flags.enableBubbleBar()
+                || SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
     }
+
     private static final int MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING
             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
index ac47c60..fe91362 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
@@ -48,7 +48,7 @@
         protected val imeSwitcher: ImageView?,
         protected val rotationButton: RotationButton?,
         protected val a11yButton: ImageView?,
-        protected val space: Space
+        protected val space: Space?
 ) : NavButtonLayoutter {
     protected val homeButton: ImageView? = navButtonContainer.findViewById(R.id.home)
     protected val recentsButton: ImageView? = navButtonContainer.findViewById(R.id.recent_apps)
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
index 7578b4c..4368b95 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
@@ -39,7 +39,7 @@
         imeSwitcher: ImageView?,
         rotationButton: RotationButton?,
         a11yButton: ImageView?,
-        space: Space,
+        space: Space?
 ) :
     AbstractNavButtonLayoutter(
             resources,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 1d4ae7b..2b60dc0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -61,7 +61,7 @@
                 imeSwitcher: ImageView?,
                 rotationButton: RotationButton?,
                 a11yButton: ImageView?,
-                space: Space,
+                space: Space?,
                 resources: Resources,
                 isKidsMode: Boolean,
                 isInSetup: Boolean,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
index 6d86f1b..bf820c0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
@@ -33,7 +33,7 @@
         imeSwitcher: ImageView?,
         rotationButton: RotationButton?,
         a11yButton: ImageView?,
-        space: Space,
+        space: Space?
 ) :
         AbstractNavButtonLayoutter(
                 resources,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index db15542..6a935f1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -36,7 +36,7 @@
         imeSwitcher: ImageView?,
         rotationButton: RotationButton?,
         a11yButton: ImageView?,
-        space: Space,
+        space: Space?
 ) :
     AbstractNavButtonLayoutter(
             resources,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
index 3eb25a2..0672270 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
@@ -36,7 +36,7 @@
         imeSwitcher: ImageView?,
         rotationButton: RotationButton?,
         a11yButton: ImageView?,
-        space: Space
+        space: Space?
 ) :
     AbstractNavButtonLayoutter(
             resources,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
index 899aab4..869cc43 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
@@ -34,7 +34,7 @@
         imeSwitcher: ImageView?,
         rotationButton: RotationButton?,
         a11yButton: ImageView?,
-        space: Space,
+        space: Space?
 ) :
         PhoneLandscapeNavLayoutter(
                 resources,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
index f31443c..181e0ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
@@ -36,7 +36,7 @@
         imeSwitcher: ImageView?,
         rotationButton: RotationButton?,
         a11yButton: ImageView?,
-        space: Space,
+        space: Space?
 ) :
     AbstractNavButtonLayoutter(
             resources,
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
index 69e524e..5c57a01 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
@@ -39,7 +39,7 @@
         imeSwitcher: ImageView?,
         rotationButton: RotationButton?,
         a11yButton: ImageView?,
-        space: Space
+        space: Space?
 ) :
     AbstractNavButtonLayoutter(
             resources,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
index 22f24f1..039c0a0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
@@ -28,10 +28,8 @@
 import android.util.Pair;
 import android.view.View;
 import android.widget.RemoteViews;
-import android.widget.Toast;
 import android.window.SplashScreen;
 
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
@@ -63,8 +61,7 @@
             // Log metric
             StatsLogManager.StatsLogger logger = mLauncher.getStatsLogManager().logger();
             logger.log(LAUNCHER_SPLIT_WIDGET_ATTEMPT);
-            Toast.makeText(hostView.getContext(), R.string.split_widgets_not_supported,
-                    Toast.LENGTH_SHORT).show();
+            mLauncher.handleIncorrectSplitTargetSelection();
             return true;
         }
         Pair<Intent, ActivityOptions> options = remoteResponse.getLaunchOptions(view);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 32d10b0..c2a248d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -36,6 +36,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
@@ -60,6 +61,7 @@
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.launcher3.Flags.enablePredictiveBackGesture;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -644,7 +646,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        if (Utilities.ATLEAST_U && FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get()) {
+        if (Utilities.ATLEAST_U && enablePredictiveBackGesture()) {
             getApplicationInfo().setEnableOnBackInvokedCallback(true);
         }
         if (savedInstanceState != null) {
@@ -676,7 +678,7 @@
                     splitSelectSource.alreadyRunningTaskId = taskWasFound
                             ? foundTask.key.id
                             : INVALID_TASK_ID;
-                    if (FeatureFlags.enableSplitContextually()) {
+                    if (enableSplitContextually()) {
                         startSplitToHome(splitSelectSource);
                     } else {
                         recentsView.initiateSplitSelect(splitSelectSource);
@@ -761,7 +763,7 @@
 
         super.onPause();
 
-        if (FeatureFlags.enableSplitContextually()) {
+        if (enableSplitContextually()) {
             // If Launcher pauses before both split apps are selected, exit split screen.
             if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
                     !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
@@ -832,7 +834,7 @@
 
     @Override
     protected void registerBackDispatcher() {
-        if (!FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get()) {
+        if (!enablePredictiveBackGesture()) {
             super.registerBackDispatcher();
             return;
         }
@@ -1352,6 +1354,15 @@
         return (mTaskbarUIController != null && mTaskbarUIController.hasBubbles());
     }
 
+    @Override
+    public boolean handleIncorrectSplitTargetSelection() {
+        if (!enableSplitContextually() || !mSplitSelectStateController.isSplitSelectActive()) {
+            return false;
+        }
+        mSplitSelectStateController.getSplitInstructionsView().goBoing();
+        return true;
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<Launcher> {
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 27224f2..e6a115a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -45,7 +45,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitAnimationTimings;
 import com.android.quickstep.views.ClearAllButton;
@@ -69,6 +69,7 @@
         super.setState(state);
         if (state.overviewUi) {
             mRecentsView.updateEmptyMessage();
+        } else {
             mRecentsView.resetTaskVisuals();
         }
         setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
@@ -130,7 +131,7 @@
         }
 
         // Create transition animations to split select
-        PagedOrientationHandler orientationHandler =
+        RecentsPagedOrientationHandler orientationHandler =
                 ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
         Pair<FloatProperty, FloatProperty> taskViewsFloat =
                 orientationHandler.getSplitSelectTaskOffset(
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index d98e608..74ba006 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -31,6 +31,8 @@
 import android.view.Window;
 import android.view.WindowManager;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
@@ -51,12 +53,12 @@
 
     private final Launcher mLauncher;
     private final SystemUiProxy mSystemUiProxy;
-    private final float mTouchSlop;
+    @VisibleForTesting final float mTouchSlop;
     private int mLastAction;
     private final SparseArray<PointF> mDownEvents;
 
     /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/
-    private boolean mCanIntercept;
+    @VisibleForTesting boolean mCanIntercept;
 
     private boolean mIsTrackpadReverseScroll;
 
@@ -85,9 +87,9 @@
 
     @Override
     public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        int action = ev.getActionMasked();
-        int idx = ev.getActionIndex();
-        int pid = ev.getPointerId(idx);
+        final int action = ev.getActionMasked();
+        final int idx = ev.getActionIndex();
+        final int pid = ev.getPointerId(idx);
         if (action == ACTION_DOWN) {
             mCanIntercept = canInterceptTouch(ev);
             if (!mCanIntercept) {
@@ -135,7 +137,6 @@
                     .log(LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN);
             setWindowSlippery(false);
             mIsTrackpadReverseScroll = false;
-            return true;
         }
         return true;
     }
@@ -149,7 +150,8 @@
      * Touches can slide out of the window but they cannot necessarily slide
      * back in (unless the other window with touch focus permits it).
      */
-    private void setWindowSlippery(boolean enable) {
+    @VisibleForTesting
+    void setWindowSlippery(boolean enable) {
         Window w = mLauncher.getWindow();
         WindowManager.LayoutParams wlp = w.getAttributes();
         if (enable) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index ec84550..ef5096b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -36,13 +36,13 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.BaseSwipeDetector;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.FlingBlockCheck;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.VibrationConstants;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -225,7 +225,8 @@
             mCurrentAnimation.dispatchOnCancel();
         }
 
-        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                mRecentsView.getPagedOrientationHandler();
         mCurrentAnimationIsGoingUp = goingUp;
         BaseDragLayer dl = mActivity.getDragLayer();
         final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
@@ -269,7 +270,8 @@
 
     @Override
     public void onDragStart(boolean start, float startDisplacement) {
-        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                mRecentsView.getPagedOrientationHandler();
         if (mCurrentAnimation == null) {
             reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
             mDisplacementShift = 0;
@@ -283,7 +285,8 @@
 
     @Override
     public boolean onDrag(float displacement) {
-        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                mRecentsView.getPagedOrientationHandler();
         float totalDisplacement = displacement + mDisplacementShift;
         boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
                 orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
@@ -346,7 +349,8 @@
         if (blockedFling) {
             fling = false;
         }
-        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                mRecentsView.getPagedOrientationHandler();
         boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
         float progress = mCurrentAnimation.getProgressFraction();
         float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 60d0e2b..6698600 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -22,7 +22,9 @@
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.app.animation.Interpolators.DECELERATE;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.app.animation.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
 import static com.android.launcher3.BaseActivity.EVENT_STARTED;
@@ -134,6 +136,7 @@
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.SwipePipToHomeAnimator;
 import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
@@ -167,6 +170,9 @@
 
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
 
+    // Fraction of the scroll and transform animation in which the current task fades out
+    private static final float KQS_TASK_FADE_ANIMATION_FRACTION = 0.4f;
+
     protected final BaseActivityInterface<S, T> mActivityInterface;
     protected final InputConsumerProxy mInputConsumerProxy;
     protected final ActivityInitListener mActivityInitListener;
@@ -900,7 +906,10 @@
             return;
         }
         mLauncherTransitionController.setProgress(
-                Math.max(mCurrentShift.value, getScaleProgressDueToScroll()), mDragLengthFactor);
+                // Immediately finish the grid transition
+                isKeyboardTaskFocusPending()
+                        ? 1f : Math.max(mCurrentShift.value, getScaleProgressDueToScroll()),
+                mDragLengthFactor);
     }
 
     /**
@@ -1349,7 +1358,9 @@
         }
         Interpolator interpolator;
         S state = mActivityInterface.stateFromGestureEndTarget(endTarget);
-        if (state.displayOverviewTasksAsGrid(mDp)) {
+        if (isKeyboardTaskFocusPending()) {
+            interpolator = EMPHASIZED;
+        } else if (state.displayOverviewTasksAsGrid(mDp)) {
             interpolator = ACCELERATE_DECELERATE;
         } else if (endTarget == RECENTS) {
             interpolator = OVERSHOOT_1_2;
@@ -1653,7 +1664,8 @@
             animatorSet.play(windowAnim);
             if (mRecentsView != null) {
                 mRecentsView.onPrepareGestureEndAnimation(
-                        animatorSet, mGestureState.getEndTarget(),
+                        mGestureState.isHandlingAtomicEvent() ? null : animatorSet,
+                        mGestureState.getEndTarget(),
                         getRemoteTaskViewSimulators());
             }
             animatorSet.setDuration(duration).setInterpolator(interpolator);
@@ -2225,6 +2237,14 @@
         }
     }
 
+    private boolean shouldLinkRecentsViewScroll() {
+        return mRecentsViewScrollLinked && !isKeyboardTaskFocusPending();
+    }
+
+    private boolean isKeyboardTaskFocusPending() {
+        return mRecentsView != null && mRecentsView.isKeyboardTaskFocusPending();
+    }
+
     private void onRecentsViewScroll() {
         if (moveWindowWithRecentsScroll()) {
             onCurrentShiftUpdated();
@@ -2457,6 +2477,44 @@
         mActivityInitListener.register();
     }
 
+    private boolean shouldFadeOutTargetsForKeyboardQuickSwitch(
+            TransformParams transformParams,
+            TaskViewSimulator taskViewSimulator,
+            float progress) {
+        RemoteAnimationTargets targets = transformParams.getTargetSet();
+        boolean fadeAppTargets = isKeyboardTaskFocusPending()
+                && targets != null
+                && targets.apps != null
+                && targets.apps.length > 0;
+        float fadeProgress = Utilities.mapBoundToRange(
+                progress,
+                /* lowerBound= */ 0f,
+                /* upperBound= */ KQS_TASK_FADE_ANIMATION_FRACTION,
+                /* toMin= */ 0f,
+                /* toMax= */ 1f,
+                LINEAR);
+        if (!fadeAppTargets || Float.compare(fadeProgress, 1f) == 0) {
+            return false;
+        }
+        SurfaceTransaction surfaceTransaction =
+                transformParams.createSurfaceParams(taskViewSimulator);
+        SurfaceControl.Transaction transaction = surfaceTransaction.getTransaction();
+
+        for (RemoteAnimationTarget app : targets.apps) {
+            transaction.setAlpha(app.leash, 1f - fadeProgress);
+            transaction.setPosition(app.leash,
+                    /* x= */ app.startBounds.left
+                            + (mActivity.getDeviceProfile().overviewPageSpacing
+                            * (mRecentsView.isRtl() ? fadeProgress : -fadeProgress)),
+                    /* y= */ 0f);
+            transaction.setScale(app.leash, 1f, 1f);
+            taskViewSimulator.taskPrimaryTranslation.value =
+                    mRecentsView.getScrollOffsetForKeyboardTaskFocus();
+            taskViewSimulator.apply(transformParams, surfaceTransaction);
+        }
+        return true;
+    }
+
     /**
      * Applies the transform on the recents animation
      */
@@ -2466,7 +2524,7 @@
         //    swipe-to-icon animation is handled by RectFSpringAnim anim
         boolean notSwipingToHome = mRecentsAnimationTargets != null
                 && mGestureState.getEndTarget() != HOME;
-        boolean setRecentsScroll = mRecentsViewScrollLinked && mRecentsView != null;
+        boolean setRecentsScroll = shouldLinkRecentsViewScroll() && mRecentsView != null;
         float progress = Math.max(mCurrentShift.value, getScaleProgressDueToScroll());
         int scrollOffset = setRecentsScroll ? mRecentsView.getScrollOffset() : 0;
         if (!mStartMovingTasks && (progress > 0 || scrollOffset != 0)) {
@@ -2485,7 +2543,12 @@
                 if (setRecentsScroll) {
                     taskViewSimulator.setScroll(scrollOffset);
                 }
-                taskViewSimulator.apply(remoteHandle.getTransformParams());
+                TransformParams transformParams = remoteHandle.getTransformParams();
+                if (shouldFadeOutTargetsForKeyboardQuickSwitch(
+                        transformParams, taskViewSimulator, progress)) {
+                    continue;
+                }
+                taskViewSimulator.apply(transformParams);
             }
         }
     }
@@ -2493,7 +2556,7 @@
     // Scaling of RecentsView during quick switch based on amount of recents scroll
     private float getScaleProgressDueToScroll() {
         if (mActivity == null || !mActivity.getDeviceProfile().isTablet || mRecentsView == null
-                || !mRecentsViewScrollLinked) {
+                || !shouldLinkRecentsViewScroll()) {
             return 0;
         }
 
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 377b866..b89d20c 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -62,6 +62,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.views.ScrimView;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.views.RecentsView;
@@ -128,7 +129,7 @@
 
     public abstract int getSwipeUpDestinationAndLength(
             DeviceProfile dp, Context context, Rect outRect,
-            PagedOrientationHandler orientationHandler);
+            RecentsPagedOrientationHandler orientationHandler);
 
     /** Called when the animation to home has fully settled. */
     public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {}
@@ -177,7 +178,7 @@
     public abstract <T extends RecentsView> T getVisibleRecentsView();
 
     @UiThread
-    public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
+    public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
 
     public abstract Rect getOverviewWindowBounds(
             Rect homeBounds, RemoteAnimationTarget target);
@@ -519,7 +520,7 @@
             // Since we are changing the start position of the UI, reapply the state, at the end
             controller.setEndAction(() -> mActivity.getStateManager().goToState(
                     controller.getInterpolatedProgress() > 0.5 ? mTargetState : mBackgroundState,
-                    false));
+                    /* animated= */ false));
 
             RecentsView recentsView = mActivity.getOverviewPanel();
             AnimatorControllerWithResistance controllerWithResistance =
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 5c96000..27e8726 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -32,10 +32,10 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.taskbar.FallbackTaskbarUIController;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.views.RecentsView;
@@ -60,7 +60,7 @@
     /** 2 */
     @Override
     public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
-            PagedOrientationHandler orientationHandler) {
+            RecentsPagedOrientationHandler orientationHandler) {
         calculateTaskSize(context, dp, outRect, orientationHandler);
         if (dp.isVerticalBarLayout() && DisplayController.getNavigationMode(context) != NO_BUTTON) {
             return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
@@ -122,7 +122,7 @@
     }
 
     @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
         return false;
     }
 
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index e77a450..b42eb06 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -371,8 +371,8 @@
                     if (mSpringAnim != null) {
                         mSpringAnim.onTargetPositionChanged();
                     }
-                    mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK);
                 }
+                mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK);
                 maybeSendEndMessage();
             } catch (Exception e) {
                 // Ignore
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index feaa063..97c48e6 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.FLOATING_SEARCH_BAR;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 
@@ -46,11 +45,11 @@
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.NavigationMode;
 import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.LayoutUtils;
@@ -74,7 +73,7 @@
 
     @Override
     public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
-            PagedOrientationHandler orientationHandler) {
+            RecentsPagedOrientationHandler orientationHandler) {
         calculateTaskSize(context, dp, outRect, orientationHandler);
         if (dp.isVerticalBarLayout()
                 && DisplayController.getNavigationMode(context) != NavigationMode.NO_BUTTON) {
@@ -212,7 +211,7 @@
     }
 
     @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
         Launcher launcher = getVisibleLauncher();
         if (launcher == null) {
             return false;
@@ -227,7 +226,7 @@
         closeOverlay();
         launcher.getStateManager().goToState(OVERVIEW,
                 launcher.getStateManager().shouldAnimateStateChange(),
-                onCompleteCallback == null ? null : forEndCallback(onCompleteCallback));
+                animatorListener);
         return true;
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 315316d..e448a14 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -15,9 +15,12 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.PagedView.INVALID_PAGE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.content.Intent;
 import android.graphics.PointF;
 import android.os.SystemClock;
@@ -25,7 +28,6 @@
 import android.view.View;
 
 import androidx.annotation.BinderThread;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
@@ -75,7 +77,7 @@
      * do not lose the focus across multiple calls of
      * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command
      */
-    private int mTaskFocusIndexOverride = -1;
+    private int mKeyboardTaskFocusIndex = -1;
 
     /**
      * Whether we should incoming toggle commands while a previous toggle command is still ongoing.
@@ -195,9 +197,11 @@
         }
         BaseActivityInterface<?, T> activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
-        RecentsView recents = activityInterface.getVisibleRecentsView();
-        if (recents == null) {
+        RecentsView visibleRecentsView = activityInterface.getVisibleRecentsView();
+        RecentsView createdRecentsView;
+        if (visibleRecentsView == null) {
             T activity = activityInterface.getCreatedActivity();
+            createdRecentsView = activity == null ? null : activity.getOverviewPanel();
             DeviceProfile dp = activity == null ? null : activity.getDeviceProfile();
             TaskbarUIController uiController = activityInterface.getTaskbarController();
             boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
@@ -209,8 +213,8 @@
                 if (!allowQuickSwitch) {
                     return true;
                 }
-                mTaskFocusIndexOverride = uiController.launchFocusedTask();
-                if (mTaskFocusIndexOverride == -1) {
+                mKeyboardTaskFocusIndex = uiController.launchFocusedTask();
+                if (mKeyboardTaskFocusIndex == -1) {
                     return true;
                 }
             }
@@ -224,34 +228,47 @@
                 return true;
             }
         } else {
+            createdRecentsView = visibleRecentsView;
             switch (cmd.type) {
                 case TYPE_SHOW:
                     // already visible
                     return true;
                 case TYPE_HIDE: {
-                    mTaskFocusIndexOverride = -1;
-                    int currentPage = recents.getNextPage();
-                    TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount())
-                            ? (TaskView) recents.getPageAt(currentPage)
+                    mKeyboardTaskFocusIndex = INVALID_PAGE;
+                    int currentPage = visibleRecentsView.getNextPage();
+                    TaskView tv = (currentPage >= 0
+                            && currentPage < visibleRecentsView.getTaskViewCount())
+                            ? (TaskView) visibleRecentsView.getPageAt(currentPage)
                             : null;
-                    return launchTask(recents, tv, cmd);
+                    return launchTask(visibleRecentsView, tv, cmd);
                 }
                 case TYPE_TOGGLE:
-                    return launchTask(recents, getNextTask(recents), cmd);
+                    return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd);
                 case TYPE_HOME:
-                    recents.startHome();
+                    visibleRecentsView.startHome();
                     return true;
             }
         }
 
-        final Runnable completeCallback = () -> {
-            RecentsView rv = activityInterface.getVisibleRecentsView();
-            if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
-                updateRecentsViewFocus(rv);
+        if (createdRecentsView != null) {
+            createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex);
+        }
+        // Handle recents view focus when launching from home
+        Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                super.onAnimationStart(animation);
+                updateRecentsViewFocus(cmd);
             }
-            scheduleNextTask(cmd);
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                onRecentsViewFocusUpdated(cmd);
+                scheduleNextTask(cmd);
+            }
         };
-        if (activityInterface.switchToRecentsIfVisible(completeCallback)) {
+        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
             // If successfully switched, wait until animation finishes
             return false;
         }
@@ -276,6 +293,7 @@
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
+                updateRecentsViewFocus(cmd);
                 activityInterface.runOnInitBackgroundStateUI(() ->
                         interactionHandler.onGestureEnded(0, new PointF()));
                 cmd.removeListener(this);
@@ -290,14 +308,12 @@
                 if (createdActivity == null) {
                     return;
                 }
-                RecentsView createdRecents = createdActivity.getOverviewPanel();
-                if (createdRecents != null) {
-                    createdRecents.onRecentsAnimationComplete();
+                if (createdRecentsView != null) {
+                    createdRecentsView.onRecentsAnimationComplete();
                 }
             }
         };
 
-        RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
         if (visibleRecentsView != null) {
             visibleRecentsView.moveRunningTaskToFront();
         }
@@ -317,7 +333,6 @@
             interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/);
             cmd.mActiveCallbacks.addListener(recentAnimListener);
         }
-
         Trace.beginAsyncSection(TRANSITION_NAME, 0);
         return false;
     }
@@ -325,47 +340,58 @@
     private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
         cmd.removeListener(handler);
         Trace.endAsyncSection(TRANSITION_NAME, 0);
-
-        RecentsView rv =
-                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
-        if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
-            updateRecentsViewFocus(rv);
-        }
+        onRecentsViewFocusUpdated(cmd);
         scheduleNextTask(cmd);
     }
 
-    private void updateRecentsViewFocus(@NonNull RecentsView rv) {
+    private void updateRecentsViewFocus(CommandInfo cmd) {
+        RecentsView recentsView =
+                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+        if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE)) {
+            return;
+        }
         // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT),
         // the touch mode somehow is not change to false by the Android framework.
         // The subsequent tab to go through tasks in overview can only be dispatched to
         // focuses views, while focus can only be requested in
         // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
         // here we launch overview with live tile.
-        rv.getViewRootImpl().touchModeChanged(false);
+        recentsView.getViewRootImpl().touchModeChanged(false);
         // Ensure that recents view has focus so that it receives the followup key inputs
-        TaskView taskView = rv.getTaskViewAt(mTaskFocusIndexOverride);
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) {
             return;
         }
-        taskView = rv.getNextTaskView();
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getNextTaskView())) {
             return;
         }
-        taskView = rv.getTaskViewAt(0);
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getTaskViewAt(0))) {
             return;
         }
-        requestFocus(rv);
+        requestFocus(recentsView);
     }
 
-    private void requestFocus(@NonNull View view) {
-        view.post(() -> {
-            view.requestFocus();
-            view.requestAccessibilityFocus();
+    private void onRecentsViewFocusUpdated(CommandInfo cmd) {
+        RecentsView recentsView =
+                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+        if (recentsView == null
+                || cmd.type != TYPE_HIDE
+                || mKeyboardTaskFocusIndex == INVALID_PAGE) {
+            return;
+        }
+        recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE);
+        recentsView.setCurrentPage(mKeyboardTaskFocusIndex);
+        mKeyboardTaskFocusIndex = INVALID_PAGE;
+    }
+
+    private boolean requestFocus(@Nullable View taskView) {
+        if (taskView == null) {
+            return false;
+        }
+        taskView.post(() -> {
+            taskView.requestFocus();
+            taskView.requestAccessibilityFocus();
         });
+        return true;
     }
 
     public void dump(PrintWriter pw) {
@@ -374,7 +400,7 @@
         if (!mPendingCommands.isEmpty()) {
             pw.println("    pendingCommandType=" + mPendingCommands.get(0).type);
         }
-        pw.println("  mTaskFocusIndexOverride=" + mTaskFocusIndexOverride);
+        pw.println("  mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex);
         pw.println("  mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete);
     }
 
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 65c8662..8535a33 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -14,14 +14,17 @@
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.testing.TestInformationHandler;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
+import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
 
+import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -37,6 +40,30 @@
     public Bundle call(String method, String arg, @Nullable Bundle extras) {
         final Bundle response = new Bundle();
         switch (method) {
+            case TestProtocol.REQUEST_RECENT_TASKS_LIST: {
+                ArrayList<String> taskBaseIntentComponents = new ArrayList<>();
+                CountDownLatch latch = new CountDownLatch(1);
+                RecentsModel.INSTANCE.get(mContext).getTasks((taskGroups) -> {
+                    for (GroupTask group : taskGroups) {
+                        taskBaseIntentComponents.add(
+                                group.task1.key.baseIntent.getComponent().flattenToString());
+                        if (group.task2 != null) {
+                            taskBaseIntentComponents.add(
+                                    group.task2.key.baseIntent.getComponent().flattenToString());
+                        }
+                    }
+                    latch.countDown();
+                });
+                try {
+                    latch.await(2, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+                response.putStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        taskBaseIntentComponents);
+                return response;
+            }
+
             case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
                 final float swipeHeight =
                         LayoutUtils.getDefaultSwipeHeight(mContext, mDeviceProfile);
@@ -56,7 +83,7 @@
                 }
                 Rect focusedTaskRect = new Rect();
                 LauncherActivityInterface.INSTANCE.calculateTaskSize(mContext, mDeviceProfile,
-                        focusedTaskRect, PagedOrientationHandler.PORTRAIT);
+                        focusedTaskRect, RecentsPagedOrientationHandler.PORTRAIT);
                 response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, focusedTaskRect.height());
                 return response;
             }
@@ -67,7 +94,7 @@
                 }
                 Rect gridTaskRect = new Rect();
                 LauncherActivityInterface.INSTANCE.calculateGridTaskSize(mContext, mDeviceProfile,
-                        gridTaskRect, PagedOrientationHandler.PORTRAIT);
+                        gridTaskRect, RecentsPagedOrientationHandler.PORTRAIT);
                 response.putParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD, gridTaskRect);
                 return response;
             }
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index e481165..b920c10 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig;
@@ -151,7 +152,7 @@
     @UiThread
     public abstract void onCurrentShiftUpdated();
 
-    protected PagedOrientationHandler getOrientationHandler() {
+    protected RecentsPagedOrientationHandler getOrientationHandler() {
         // OrientationHandler should be independent of remote target, can directly take one
         return mRemoteTargetHandles[0].getTaskViewSimulator()
                 .getOrientationState().getOrientationHandler();
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 4f885af..a1a3145 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -66,6 +66,8 @@
     private Runnable mLiveTileCleanUpHandler;
     private Context mCtx;
 
+    private boolean mRecentsAnimationStartPending = false;
+
     private final TaskStackChangeListener mLiveTileRestartListener = new TaskStackChangeListener() {
         @Override
         public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
@@ -100,6 +102,10 @@
                 .startRecentsActivity(intent, 0, null, null, null));
     }
 
+    public boolean isRecentsAnimationStartPending() {
+        return mRecentsAnimationStartPending;
+    }
+
     /**
      * Starts a new recents animation for the activity with the given {@param intent}.
      */
@@ -109,6 +115,7 @@
         ActiveGestureLog.INSTANCE.addLog(
                 /* event= */ "startRecentsAnimation",
                 /* gestureEvent= */ START_RECENTS_ANIMATION);
+        mRecentsAnimationStartPending = true;
         // Notify if recents animation is still running
         if (mController != null) {
             String msg = "New recents animation started before old animation completed";
@@ -138,6 +145,7 @@
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
+                mRecentsAnimationStartPending = false;
                 if (mCallbacks == null) {
                     // It's possible for the recents animation to have finished and be cleaned up
                     // by the time we process the start callback, and in that case, just we can skip
@@ -178,11 +186,13 @@
 
             @Override
             public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                mRecentsAnimationStartPending = false;
                 cleanUpRecentsAnimation(newCallbacks);
             }
 
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+                mRecentsAnimationStartPending = false;
                 cleanUpRecentsAnimation(newCallbacks);
             }
 
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 73b786f..9c84df8 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -47,9 +47,9 @@
 import com.android.launcher3.model.WellbeingModel;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.popup.SystemShortcut.AppInfo;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskThumbnailView;
@@ -281,7 +281,7 @@
             final int intentFlags = task.key.baseIntent.getFlags();
             final TaskView taskView = taskContainer.getTaskView();
             final RecentsView recentsView = taskView.getRecentsView();
-            final PagedOrientationHandler orientationHandler =
+            final RecentsPagedOrientationHandler orientationHandler =
                     recentsView.getPagedOrientationHandler();
 
             boolean notEnoughTasksToSplit =
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 55a7985..5228420 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,6 +24,7 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.useActivityOverlay;
 import static com.android.launcher3.Launcher.INTENT_ACTION_ALL_APPS_TOGGLE;
 import static com.android.launcher3.LauncherPrefs.backedUpItem;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -39,6 +40,7 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
@@ -697,7 +699,7 @@
     private void onInputEvent(InputEvent ev) {
         if (!(ev instanceof MotionEvent)) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event, received unknown event ")
+                    .append("Cannot process input event: received unknown event ")
                     .append(ev.toString()));
             return;
         }
@@ -710,22 +712,32 @@
         if (!isUserUnlocked || (mDeviceState.isButtonNavMode()
                 && !isTrackpadMotionEvent(event))) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event, ")
+                    .append("Cannot process input event: ")
                     .append(!isUserUnlocked
                             ? "user is locked"
                             : "using 3-button nav and event is not a trackpad event"));
             return;
         }
 
-        SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
-
         final int action = event.getActionMasked();
         // Note this will create a new consumer every mouse click, as after ACTION_UP from the click
         // an ACTION_HOVER_ENTER will fire as well.
         boolean isHoverActionWithoutConsumer = enableCursorHoverStates()
                 && isHoverActionWithoutConsumer(event);
+        if (mTaskAnimationManager.isRecentsAnimationStartPending()
+                && (action == ACTION_DOWN || isHoverActionWithoutConsumer)) {
+            ActiveGestureLog.INSTANCE.addLog(
+                    new CompoundString("TIS.onInputEvent: ")
+                            .append("Cannot process input event: a recents animation has been ")
+                            .append("requested, but hasn't started."),
+                    RECENTS_ANIMATION_START_PENDING);
+            return;
+        }
+
+        SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
+
         CompoundString reasonString = action == ACTION_DOWN
-                ? new CompoundString("onMotionEvent: ") : CompoundString.NO_OP;
+                ? new CompoundString("TIS.onMotionEvent: ") : CompoundString.NO_OP;
         if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
             mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
@@ -1148,6 +1160,14 @@
         boolean launcherResumedThroughShellTransition =
                 gestureState.getActivityInterface().isResumed()
                         && !previousGestureState.isRecentsAnimationRunning();
+        // If a task fragment within Launcher is resumed
+        boolean launcherChildActivityResumed = useActivityOverlay()
+                && runningTask != null
+                && runningTask.isHomeTask()
+                && mOverviewComponentObserver.isHomeAndOverviewSame()
+                && !launcherResumedThroughShellTransition
+                && !previousGestureState.isRecentsAnimationRunning();
+
         if (gestureState.getActivityInterface().isInLiveTileMode()) {
             return createOverviewInputConsumer(
                     previousGestureState,
@@ -1174,9 +1194,11 @@
                                             ? "launcher resumed through a shell transition"
                                             : "forceOverviewInputConsumer == true"))
                             .append(", trying to use overview input consumer"));
-        } else if (mDeviceState.isGestureBlockedTask(runningTask)) {
+        } else if (mDeviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) {
             return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX)
-                    .append("is gesture-blocked task, trying to use default input consumer"));
+                    .append(launcherChildActivityResumed
+                            ? "is launcher child-task, trying to use default input consumer"
+                            : "is gesture-blocked task, trying to use default input consumer"));
         } else {
             reasonString.append(SUBSTRING_PREFIX)
                     .append("using OtherActivityInputConsumer");
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index c8c7dc2..0f8ceba 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -154,6 +154,7 @@
         mSquaredTouchSlop = mDeviceState.getSquaredTouchSlop();
 
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
+        mStartDisplacement = continuingPreviousGesture ? 0 : -mTouchSlop;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
     }
@@ -280,7 +281,7 @@
                         if (mGestureState.isTrackpadGesture() || Math.abs(displacement)
                                 > mTouchSlop) {
                             mPassedWindowMoveSlop = true;
-                            mStartDisplacement = Math.min(displacement, -mTouchSlop);
+                            mStartDisplacement = -mTouchSlop;
                         }
                     }
                 }
@@ -336,7 +337,7 @@
                         }
                         if (!mPassedWindowMoveSlop) {
                             mPassedWindowMoveSlop = true;
-                            mStartDisplacement = Math.min(displacement, -mTouchSlop);
+                            mStartDisplacement = -mTouchSlop;
                         }
                         notifyGestureStarted(isLikelyToStartNewTask);
                     }
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
similarity index 91%
rename from src/com/android/launcher3/touch/LandscapePagedViewHandler.java
rename to quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
index f7afcb9..a7ca515 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.touch;
+package com.android.quickstep.orientation;
 
 import static android.view.Gravity.BOTTOM;
 import static android.view.Gravity.CENTER_VERTICAL;
@@ -36,6 +36,7 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
 
 import android.content.res.Resources;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -54,16 +55,18 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.views.IconAppChipView;
 
 import java.util.Collections;
 import java.util.List;
 
-public class LandscapePagedViewHandler implements PagedOrientationHandler {
+public class LandscapePagedViewHandler implements RecentsPagedOrientationHandler {
 
     @Override
     public <T> T getPrimaryValue(T x, T y) {
@@ -398,7 +401,7 @@
 
     @Override
     public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
-        boolean layoutChild) {
+            boolean layoutChild) {
         final int childHeight = child.getMeasuredHeight();
         final int childBottom = childStart + childHeight;
         final int childWidth = child.getMeasuredWidth();
@@ -531,34 +534,52 @@
         int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
                 ? splitBoundsConfig.dividerHeightPercent
                 : splitBoundsConfig.dividerWidthPercent));
-        int primarySnapshotHeight;
-        int primarySnapshotWidth;
-        int secondarySnapshotHeight;
-        int secondarySnapshotWidth;
 
-        float taskPercent = splitBoundsConfig.appsStackedVertically ?
-                splitBoundsConfig.topTaskPercent : splitBoundsConfig.leftTaskPercent;
-        primarySnapshotWidth = parentWidth;
-        primarySnapshotHeight = (int) (totalThumbnailHeight * (taskPercent));
+        Pair<Point, Point> taskViewSizes =
+                getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
 
-        secondarySnapshotWidth = parentWidth;
-        secondarySnapshotHeight = totalThumbnailHeight - primarySnapshotHeight - dividerBar;
-
-        int translationY = primarySnapshotHeight + spaceAboveSnapshot + dividerBar;
+        int translationY = taskViewSizes.first.y + spaceAboveSnapshot + dividerBar;
         primarySnapshot.setTranslationY(spaceAboveSnapshot);
         secondarySnapshot.setTranslationY(translationY - spaceAboveSnapshot);
 
         primarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY)
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)
         );
         secondarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight, View.MeasureSpec.EXACTLY)
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY)
         );
     }
 
     @Override
+    public Pair<Point, Point> getGroupedTaskViewSizes(
+            DeviceProfile dp,
+            SplitBounds splitBoundsConfig,
+            int parentWidth,
+            int parentHeight) {
+        int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
+        int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
+        int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
+                ? splitBoundsConfig.dividerHeightPercent
+                : splitBoundsConfig.dividerWidthPercent));
+        float taskPercent = splitBoundsConfig.appsStackedVertically
+                ? splitBoundsConfig.topTaskPercent
+                : splitBoundsConfig.leftTaskPercent;
+
+        Point firstTaskViewSize = new Point(
+                parentWidth,
+                (int) (totalThumbnailHeight * taskPercent)
+        );
+        Point secondTaskViewSize = new Point(
+                parentWidth,
+                totalThumbnailHeight - firstTaskViewSize.y - dividerBar
+        );
+
+        return new Pair<>(firstTaskViewSize, secondTaskViewSize);
+    }
+
+    @Override
     public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
             int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
         if (enableOverviewIconMenu()) {
@@ -574,21 +595,21 @@
     }
 
     @Override
-    public void setIconAppChipMenuParams(View iconAppChipMenuView,
+    public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
             FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
-        boolean isRtl = iconAppChipMenuView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
         iconMenuParams.gravity = (isRtl ? START : END) | (isRtl ? BOTTOM : TOP);
         iconMenuParams.setMarginStart(isRtl ? iconMenuMargin : 0);
         iconMenuParams.topMargin = iconMenuMargin;
         iconMenuParams.bottomMargin = isRtl ? iconMenuMargin : 0;
         iconMenuParams.setMarginEnd(iconMenuMargin);
 
-        iconAppChipMenuView.setPivotX(isRtl ? iconMenuParams.width - (iconMenuParams.height / 2f)
+        iconAppChipView.setPivotX(isRtl ? iconMenuParams.width - (iconMenuParams.height / 2f)
                 : iconMenuParams.width / 2f);
-        iconAppChipMenuView.setPivotY(
+        iconAppChipView.setPivotY(
                 isRtl ? (iconMenuParams.height / 2f) : iconMenuParams.width / 2f);
-        iconAppChipMenuView.setTranslationY(0);
-        iconAppChipMenuView.setRotation(getDegreesRotated());
+        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setRotation(getDegreesRotated());
     }
 
     @Override
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
similarity index 86%
rename from src/com/android/launcher3/touch/PortraitPagedViewHandler.java
rename to quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 8301981..5d9a668 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.touch;
+package com.android.quickstep.orientation;
 
 import static android.view.Gravity.BOTTOM;
 import static android.view.Gravity.CENTER_HORIZONTAL;
@@ -33,8 +33,8 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
 
-import android.content.res.Resources;
 import android.graphics.Matrix;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -42,26 +42,27 @@
 import android.util.FloatProperty;
 import android.util.Pair;
 import android.view.Gravity;
-import android.view.MotionEvent;
 import android.view.Surface;
-import android.view.VelocityTracker;
 import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.touch.DefaultPagedViewHandler;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+import com.android.quickstep.views.IconAppChipView;
 
 import java.util.ArrayList;
 import java.util.List;
 
-public class PortraitPagedViewHandler implements PagedOrientationHandler {
+public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements
+        RecentsPagedOrientationHandler {
 
     private final Matrix mTmpMatrix = new Matrix();
     private final RectF mTmpRectF = new RectF();
@@ -75,27 +76,6 @@
     public <T> T getSecondaryValue(T x, T y) {
         return y;
     }
-
-    @Override
-    public int getPrimaryValue(int x, int y) {
-        return x;
-    }
-
-    @Override
-    public int getSecondaryValue(int x, int y) {
-        return y;
-    }
-
-    @Override
-    public float getPrimaryValue(float x, float y) {
-        return x;
-    }
-
-    @Override
-    public float getSecondaryValue(float x, float y) {
-        return y;
-    }
-
     @Override
     public boolean isLayoutNaturalToLauncher() {
         return true;
@@ -116,16 +96,6 @@
     }
 
     @Override
-    public <T> void setPrimary(T target, Int2DAction<T> action, int param) {
-        action.call(target, param, 0);
-    }
-
-    @Override
-    public <T> void setPrimary(T target, Float2DAction<T> action, float param) {
-        action.call(target, param, 0);
-    }
-
-    @Override
     public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
         action.call(target, 0, param);
     }
@@ -137,21 +107,6 @@
     }
 
     @Override
-    public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
-        return event.getX(pointerIndex);
-    }
-
-    @Override
-    public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) {
-        return velocityTracker.getXVelocity(pointerId);
-    }
-
-    @Override
-    public int getMeasuredSize(View view) {
-        return view.getMeasuredWidth();
-    }
-
-    @Override
     public int getPrimarySize(View view) {
         return view.getWidth();
     }
@@ -192,26 +147,6 @@
     }
 
     @Override
-    public int getPrimaryScroll(View view) {
-        return view.getScrollX();
-    }
-
-    @Override
-    public float getPrimaryScale(View view) {
-        return view.getScaleX();
-    }
-
-    @Override
-    public void setMaxScroll(AccessibilityEvent event, int maxScroll) {
-        event.setMaxScrollX(maxScroll);
-    }
-
-    @Override
-    public boolean getRecentsRtlSetting(Resources resources) {
-        return !Utilities.isRtl(resources);
-    }
-
-    @Override
     public float getDegreesRotated() {
         return 0;
     }
@@ -231,27 +166,6 @@
         view.setScaleY(scale);
     }
 
-    @Override
-    public int getChildStart(View view) {
-        return view.getLeft();
-    }
-
-    @Override
-    public int getCenterForPage(View view, Rect insets) {
-        return (view.getPaddingTop() + view.getMeasuredHeight() + insets.top
-            - insets.bottom - view.getPaddingBottom()) / 2;
-    }
-
-    @Override
-    public int getScrollOffsetStart(View view, Rect insets) {
-        return insets.left + view.getPaddingLeft();
-    }
-
-    @Override
-    public int getScrollOffsetEnd(View view, Rect insets) {
-        return view.getWidth() - view.getPaddingRight() - insets.right;
-    }
-
     public int getSecondaryTranslationDirectionFactor() {
         return -1;
     }
@@ -400,20 +314,6 @@
     }
 
     /* -------------------- */
-
-    @Override
-    public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
-        boolean layoutChild) {
-        final int childWidth = child.getMeasuredWidth();
-        final int childRight = childStart + childWidth;
-        final int childHeight = child.getMeasuredHeight();
-        final int childTop = pageCenter - childHeight / 2;
-        if (layoutChild) {
-            child.layout(childStart, childTop, childRight, childTop + childHeight);
-        }
-        return new ChildBounds(childWidth, childHeight, childRight, childTop);
-    }
-
     @Override
     public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
         return dp.heightPx - rect.bottom;
@@ -631,25 +531,16 @@
         float dividerScale = splitBoundsConfig.appsStackedVertically
                 ? splitBoundsConfig.dividerHeightPercent
                 : splitBoundsConfig.dividerWidthPercent;
-        int primarySnapshotHeight;
-        int primarySnapshotWidth;
-        int secondarySnapshotHeight;
-        int secondarySnapshotWidth;
-        float taskPercent = splitBoundsConfig.appsStackedVertically ?
-                splitBoundsConfig.topTaskPercent : splitBoundsConfig.leftTaskPercent;
+        Pair<Point, Point> taskViewSizes =
+                getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
         if (dp.isLeftRightSplit) {
             int scaledDividerBar = Math.round(parentWidth * dividerScale);
-            primarySnapshotHeight = totalThumbnailHeight;
-            primarySnapshotWidth = Math.round(parentWidth * taskPercent);
-
-            secondarySnapshotHeight = totalThumbnailHeight;
-            secondarySnapshotWidth = parentWidth - primarySnapshotWidth - scaledDividerBar;
             if (isRtl) {
-                int translationX = secondarySnapshotWidth + scaledDividerBar;
+                int translationX = taskViewSizes.second.x + scaledDividerBar;
                 primarySnapshot.setTranslationX(-translationX);
                 secondarySnapshot.setTranslationX(0);
             } else {
-                int translationX = primarySnapshotWidth + scaledDividerBar;
+                int translationX = taskViewSizes.first.x + scaledDividerBar;
                 secondarySnapshot.setTranslationX(translationX);
                 primarySnapshot.setTranslationX(0);
             }
@@ -658,18 +549,8 @@
             // Reset unused translations
             primarySnapshot.setTranslationY(0);
         } else {
-            int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight;
-            float scale = (float) totalThumbnailHeight / (dp.availableHeightPx - taskbarHeight);
-            float topTaskHeight = dp.availableHeightPx * taskPercent;
             float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
-            float scaledTopTaskHeight = topTaskHeight * scale;
-            primarySnapshotWidth = parentWidth;
-            primarySnapshotHeight = Math.round(scaledTopTaskHeight);
-
-            secondarySnapshotWidth = parentWidth;
-            secondarySnapshotHeight = Math.round(totalThumbnailHeight - primarySnapshotHeight
-                    - finalDividerHeight);
-            float translationY = primarySnapshotHeight + spaceAboveSnapshot + finalDividerHeight;
+            float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight;
             secondarySnapshot.setTranslationY(translationY);
 
             FrameLayout.LayoutParams primaryParams =
@@ -685,11 +566,11 @@
             primarySnapshot.setTranslationX(0);
         }
         primarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY));
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY));
         secondarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight,
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y,
                         View.MeasureSpec.EXACTLY));
         primarySnapshot.setScaleX(1);
         secondarySnapshot.setScaleX(1);
@@ -698,6 +579,48 @@
     }
 
     @Override
+    public Pair<Point, Point> getGroupedTaskViewSizes(
+            DeviceProfile dp,
+            SplitBounds splitBoundsConfig,
+            int parentWidth,
+            int parentHeight) {
+        int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
+        int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
+        float dividerScale = splitBoundsConfig.appsStackedVertically
+                ? splitBoundsConfig.dividerHeightPercent
+                : splitBoundsConfig.dividerWidthPercent;
+        float taskPercent = splitBoundsConfig.appsStackedVertically
+                ? splitBoundsConfig.topTaskPercent
+                : splitBoundsConfig.leftTaskPercent;
+
+        Point firstTaskViewSize = new Point();
+        Point secondTaskViewSize = new Point();
+
+        if (dp.isLeftRightSplit) {
+            int scaledDividerBar = Math.round(parentWidth * dividerScale);
+            firstTaskViewSize.x = Math.round(parentWidth * taskPercent);
+            firstTaskViewSize.y = totalThumbnailHeight;
+
+            secondTaskViewSize.x = parentWidth - firstTaskViewSize.x - scaledDividerBar;
+            secondTaskViewSize.y = totalThumbnailHeight;
+        } else {
+            int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight;
+            float scale = (float) totalThumbnailHeight / (dp.availableHeightPx - taskbarHeight);
+            float topTaskHeight = dp.availableHeightPx * taskPercent;
+            float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale);
+            float scaledTopTaskHeight = topTaskHeight * scale;
+            firstTaskViewSize.x = parentWidth;
+            firstTaskViewSize.y = Math.round(scaledTopTaskHeight);
+
+            secondTaskViewSize.x = parentWidth;
+            secondTaskViewSize.y = Math.round(totalThumbnailHeight - firstTaskViewSize.y
+                    - finalDividerHeight);
+        }
+
+        return new Pair<>(firstTaskViewSize, secondTaskViewSize);
+    }
+
+    @Override
     public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
             int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
         if (enableOverviewIconMenu()) {
@@ -713,7 +636,7 @@
     }
 
     @Override
-    public void setIconAppChipMenuParams(View iconAppChipMenuView,
+    public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
             FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
         iconMenuParams.gravity = TOP | START;
         iconMenuParams.setMarginStart(iconMenuMargin);
@@ -721,10 +644,10 @@
         iconMenuParams.bottomMargin = 0;
         iconMenuParams.setMarginEnd(0);
 
-        iconAppChipMenuView.setPivotX(0);
-        iconAppChipMenuView.setPivotY(0);
-        iconAppChipMenuView.setTranslationY(0);
-        iconAppChipMenuView.setRotation(getDegreesRotated());
+        iconAppChipView.setPivotX(0);
+        iconAppChipView.setPivotY(0);
+        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setRotation(getDegreesRotated());
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
new file mode 100644
index 0000000..9084297
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
@@ -0,0 +1,251 @@
+/*
+ * 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.orientation;
+
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.ShapeDrawable;
+import android.util.FloatProperty;
+import android.util.Pair;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+import com.android.quickstep.views.IconAppChipView;
+
+import java.util.List;
+
+/**
+ * Abstraction layer to separate horizontal and vertical specific implementations
+ * for {@link com.android.quickstep.views.RecentsView}. Majority of these implementations are
+ * (should be) as simple as choosing the correct X and Y analogous methods.
+ */
+public interface RecentsPagedOrientationHandler extends PagedOrientationHandler {
+
+    RecentsPagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler();
+    RecentsPagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler();
+    RecentsPagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler();
+
+    <T> void setSecondary(T target, Float2DAction<T> action, float param);
+    <T> void set(T target, Int2DAction<T> action, int primaryParam, int secondaryParam);
+    int getPrimarySize(View view);
+    float getPrimarySize(RectF rect);
+    int getSecondaryTranslationDirectionFactor();
+    float getDegreesRotated();
+    int getRotation();
+    boolean isLayoutNaturalToLauncher();
+
+    <T> T getPrimaryValue(T x, T y);
+    <T> T getSecondaryValue(T x, T y);
+    void setPrimaryScale(View view, float scale);
+    void setSecondaryScale(View view, float scale);
+    float getStart(RectF rect);
+    float getEnd(RectF rect);
+    int getClearAllSidePadding(View view, boolean isRtl);
+    int getSecondaryDimension(View view);
+    FloatProperty<View> getPrimaryViewTranslate();
+    FloatProperty<View> getSecondaryViewTranslate();
+    int getSplitTranslationDirectionFactor(@StagePosition int stagePosition,
+            DeviceProfile deviceProfile);
+    Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
+            FloatProperty secondary, DeviceProfile deviceProfile);
+    int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
+    List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
+    /**
+     * @param placeholderHeight height of placeholder view in portrait, width in landscape
+     */
+    void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset,
+            DeviceProfile dp, @StagePosition int stagePosition, Rect out);
+
+    /**
+     * Centers an icon in the split staging area, accounting for insets.
+     * @param out The icon that needs to be centered.
+     * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is
+     *                        offscreen).
+     * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is
+     *                        offscreen).
+     * @param fullscreenScaleX A x-scaling factor used to convert coordinates back into pixels.
+     * @param fullscreenScaleY A y-scaling factor used to convert coordinates back into pixels.
+     * @param drawableWidth The icon's drawable (final) width.
+     * @param drawableHeight The icon's drawable (final) height.
+     * @param dp The device profile, used to report rotation and hardware insets.
+     * @param stagePosition 0 if the staging area is pinned to top/left, 1 for bottom/right.
+     */
+    void updateSplitIconParams(View out, float onScreenRectCenterX,
+            float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY,
+            int drawableWidth, int drawableHeight, DeviceProfile dp,
+            @StagePosition int stagePosition);
+
+    /**
+     * Sets positioning and rotation for a SplitInstructionsView.
+     * @param out The SplitInstructionsView that needs to be positioned.
+     * @param dp The device profile, used to report rotation and device type.
+     * @param splitInstructionsHeight The SplitInstructionView's height.
+     * @param splitInstructionsWidth  The SplitInstructionView's width.
+     */
+    void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
+            int splitInstructionsWidth);
+
+    /**
+     * @param splitDividerSize height of split screen drag handle in portrait, width in landscape
+     * @param stagePosition the split position option (top/left, bottom/right) of the first
+     *                           task selected for entering split
+     * @param out1 the bounds for where the first selected app will be
+     * @param out2 the bounds for where the second selected app will be, complimentary to
+     *             {@param out1} based on {@param initialSplitOption}
+     */
+    void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
+            @StagePosition int stagePosition, Rect out1, Rect out2);
+
+    int getDefaultSplitPosition(DeviceProfile deviceProfile);
+
+    /**
+     * @param outRect This is expected to be the rect that has the dimensions for a non-split,
+     *                fullscreen task in overview. This will directly be modified.
+     * @param desiredStagePosition Which stage position (topLeft/rightBottom) we want to resize
+     *                           outRect for
+     */
+    void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo,
+            @SplitConfigurationOptions.StagePosition int desiredStagePosition);
+
+    void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
+            int parentWidth, int parentHeight,
+            SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl);
+
+    /**
+     * Creates two Points representing the dimensions of the two tasks in a GroupedTaskView
+     *
+     * @return first -> primary task snapshot, second -> secondary task snapshot.
+     * x -> width, y -> height
+     */
+    Pair<Point, Point> getGroupedTaskViewSizes(DeviceProfile dp, SplitBounds splitBoundsConfig,
+            int parentWidth, int parentHeight);
+
+    // Overview TaskMenuView methods
+    void setTaskIconParams(FrameLayout.LayoutParams iconParams,
+            int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl);
+
+    void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
+            FrameLayout.LayoutParams iconMenuParams,
+            int iconMenuMargin, int thumbnailTopMargin);
+    void setSplitIconParams(View primaryIconView, View secondaryIconView,
+            int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
+            int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
+            DeviceProfile deviceProfile, SplitBounds splitConfig);
+
+    /*
+     * The following two methods try to center the TaskMenuView in landscape by finding the center
+     * of the thumbnail view and then subtracting half of the taskMenu width. In this case, the
+     * taskMenu width is the same size as the thumbnail width (what got set below in
+     * getTaskMenuWidth()), so we directly use that in the calculations.
+     */
+    float getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile,
+            float taskInsetMargin, View taskViewIcon);
+    float getTaskMenuY(float y, View thumbnailView, int stagePosition,
+            View taskMenuView, float taskInsetMargin, View taskViewIcon);
+    int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
+            @StagePosition int stagePosition);
+    /**
+     * Sets linear layout orientation for {@link com.android.launcher3.popup.SystemShortcut} items
+     * inside task menu view.
+     */
+    void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
+            LinearLayout taskMenuLayout, int dividerSpacing,
+            ShapeDrawable dividerDrawable);
+    /**
+     * Sets layout param attributes for {@link com.android.launcher3.popup.SystemShortcut} child
+     * views inside task menu view.
+     */
+    void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
+            LinearLayout viewGroup, DeviceProfile deviceProfile);
+
+    /**
+     * Calculates the position where a Digital Wellbeing Banner should be placed on its parent
+     * TaskView.
+     * @return A Pair of Floats representing the proper x and y translations.
+     */
+    Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
+            int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
+            View[] thumbnailViews, int desiredTaskId, View banner);
+
+    // The following are only used by TaskViewTouchHandler.
+    /** @return Either VERTICAL or HORIZONTAL. */
+    SingleAxisSwipeDetector.Direction getUpDownSwipeDirection();
+    /** @return Given {@link #getUpDownSwipeDirection()}, whether POSITIVE or NEGATIVE is up. */
+    int getUpDirection(boolean isRtl);
+    /** @return Whether the displacement is going towards the top of the screen. */
+    boolean isGoingUp(float displacement, boolean isRtl);
+    /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
+    int getTaskDragDisplacementFactor(boolean isRtl);
+
+    /**
+     * Maps the velocity from the coordinate plane of the foreground app to that
+     * of Launcher's (which now will always be portrait)
+     */
+    void adjustFloatingIconStartVelocity(PointF velocity);
+
+    /**
+     * Ensures that outStartRect left bound is within the DeviceProfile's visual boundaries
+     * @param outStartRect The start rect that will directly be modified
+     */
+    void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile);
+
+    /**
+     * Determine the target translation for animating the FloatingTaskView out. This value could
+     * either be an x-coordinate or a y-coordinate, depending on which way the FloatingTaskView was
+     * docked.
+     *
+     * @param floatingTask The FloatingTaskView.
+     * @param onScreenRect The current on-screen dimensions of the FloatingTaskView.
+     * @param stagePosition STAGE_POSITION_TOP_OR_LEFT or STAGE_POSITION_BOTTOM_OR_RIGHT.
+     * @param dp The device profile.
+     * @return A float. When an animation translates the FloatingTaskView to this position, it will
+     * appear to tuck away off the edge of the screen.
+     */
+    float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect,
+            @StagePosition int stagePosition, DeviceProfile dp);
+
+    /**
+     * Sets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
+     * either x or y), depending on how the view is oriented.
+     *
+     * @param floatingTask The FloatingTaskView to be translated.
+     * @param translation The target translation value.
+     * @param dp The current device profile.
+     */
+    void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, DeviceProfile dp);
+
+    /**
+     * Gets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
+     * either x or y), depending on how the view is oriented.
+     *
+     * @param floatingTask The FloatingTaskView in question.
+     * @param dp The current device profile.
+     * @return The current translation value.
+     */
+    Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp);
+}
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
similarity index 86%
rename from src/com/android/launcher3/touch/SeascapePagedViewHandler.java
rename to quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
index 7077ad9..f3001fc 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.touch;
+package com.android.quickstep.orientation;
 
 import static android.view.Gravity.BOTTOM;
 import static android.view.Gravity.CENTER_VERTICAL;
@@ -31,6 +31,7 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
 
 import android.content.res.Resources;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.Pair;
@@ -42,10 +43,12 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.views.IconAppChipView;
 
 import java.util.Collections;
 import java.util.List;
@@ -234,9 +237,9 @@
     }
 
     @Override
-    public void setIconAppChipMenuParams(View iconAppChipMenuView,
+    public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
             FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
-        boolean isRtl = iconAppChipMenuView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
         iconMenuParams.gravity = (isRtl ? TOP : BOTTOM) | (isRtl ? END : START);
         iconMenuParams.setMarginStart(0);
         iconMenuParams.topMargin = isRtl ? iconMenuMargin : 0;
@@ -244,12 +247,12 @@
         iconMenuParams.setMarginEnd(isRtl ? thumbnailTopMargin : 0);
 
         // Use half menu height to place the pivot within the X/Y center of icon in the menu.
-        float iconCenter = iconAppChipMenuView.getHeight() / 2f;
-        iconAppChipMenuView.setPivotX(isRtl ? iconMenuParams.width / 2f : iconCenter);
-        iconAppChipMenuView.setPivotY(
+        float iconCenter = iconAppChipView.getHeight() / 2f;
+        iconAppChipView.setPivotX(isRtl ? iconMenuParams.width / 2f : iconCenter);
+        iconAppChipView.setPivotY(
                 isRtl ? iconMenuParams.width / 2f : iconCenter - iconMenuMargin);
-        iconAppChipMenuView.setTranslationY(0);
-        iconAppChipMenuView.setRotation(getDegreesRotated());
+        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setRotation(getDegreesRotated());
     }
 
     @Override
@@ -340,31 +343,52 @@
         int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
                 ? splitBoundsConfig.dividerHeightPercent
                 : splitBoundsConfig.dividerWidthPercent));
-        int primarySnapshotHeight;
-        int primarySnapshotWidth;
-        int secondarySnapshotHeight;
-        int secondarySnapshotWidth;
 
-        float taskPercent = splitBoundsConfig.appsStackedVertically ?
-                splitBoundsConfig.topTaskPercent : splitBoundsConfig.leftTaskPercent;
-        primarySnapshotWidth = parentWidth;
-        primarySnapshotHeight = (int) (totalThumbnailHeight * (taskPercent));
+        Pair<Point, Point> taskViewSizes =
+                getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
 
-        secondarySnapshotWidth = parentWidth;
-        secondarySnapshotHeight = totalThumbnailHeight - primarySnapshotHeight - dividerBar;
         secondarySnapshot.setTranslationY(0);
-        primarySnapshot.setTranslationY(secondarySnapshotHeight + spaceAboveSnapshot + dividerBar);
+        primarySnapshot.setTranslationY(taskViewSizes.second.y + spaceAboveSnapshot + dividerBar);
 
         primarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY)
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)
         );
         secondarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight, View.MeasureSpec.EXACTLY)
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY)
         );
     }
 
+    @Override
+    public Pair<Point, Point> getGroupedTaskViewSizes(
+            DeviceProfile dp,
+            SplitBounds splitBoundsConfig,
+            int parentWidth,
+            int parentHeight) {
+        // Measure and layout the thumbnails bottom up, since the primary is on the visual left
+        // (portrait bottom) and secondary is on the right (portrait top)
+        int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
+        int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
+        int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
+                ? splitBoundsConfig.dividerHeightPercent
+                : splitBoundsConfig.dividerWidthPercent));
+        float taskPercent = splitBoundsConfig.appsStackedVertically
+                ? splitBoundsConfig.topTaskPercent
+                : splitBoundsConfig.leftTaskPercent;
+
+        Point firstTaskViewSize = new Point(
+                parentWidth,
+                (int) (totalThumbnailHeight * taskPercent)
+        );
+        Point secondTaskViewSize = new Point(
+                parentWidth,
+                totalThumbnailHeight - firstTaskViewSize.y - dividerBar
+        );
+
+        return new Pair<>(firstTaskViewSize, secondTaskViewSize);
+    }
+
     /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 4359294..e3772bd 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -39,7 +39,7 @@
         SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT,
         SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
         FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
-        INVALID_VELOCITY_ON_SWIPE_UP,
+        INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
 
         /**
          * These GestureEvents are specifically associated to state flags that get set in
@@ -273,6 +273,14 @@
                 case START_RECENTS_ANIMATION:
                     lastStartRecentAnimationEventEntryTime = eventEntry.getTime();
                     break;
+                case RECENTS_ANIMATION_START_PENDING:
+                    errorDetected |= printErrorIfTrue(
+                            true,
+                            prefix,
+                            /* errorMessage= */ "new gesture attempted while a requested recents"
+                                    + " animation is still pending.",
+                            writer);
+                    break;
                 case EXPECTING_TASK_APPEARED:
                 case MOTION_DOWN:
                 case SET_END_TARGET:
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index cb35ec8..bc8b571 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -43,7 +43,7 @@
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.AllAppsSwipeController;
-import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -200,7 +200,7 @@
     public static <SCALE, TRANSLATION> PendingAnimation createRecentsResistanceAnim(
             RecentsParams<SCALE, TRANSLATION> params) {
         Rect startRect = new Rect();
-        PagedOrientationHandler orientationHandler = params.recentsOrientedState
+        RecentsPagedOrientationHandler orientationHandler = params.recentsOrientedState
                 .getOrientationHandler();
         params.recentsOrientedState.getActivityInterface()
                 .calculateTaskSize(params.context, params.dp, startRect, orientationHandler);
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 0f3c029..839320e 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -27,6 +27,7 @@
 import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.Intent;
+import android.util.Log;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -58,6 +59,8 @@
  * ratio.
  */
 public class AppPairsController {
+    private static final String TAG = "AppPairsController";
+
     // Used for encoding and decoding the "rank" attribute
     private static final int BITMASK_SIZE = 16;
     private static final int BITMASK_FOR_SNAP_POSITION = (1 << BITMASK_SIZE) - 1;
@@ -89,13 +92,23 @@
 
         @PersistentSnapPosition int snapPosition = gtv.getSnapPosition();
         if (!isPersistentSnapPosition(snapPosition)) {
-            throw new RuntimeException("tried to save an app pair with illegal 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 " + snapPosition);
+            return;
         }
 
         app1.rank = encodeRank(SPLIT_POSITION_TOP_OR_LEFT, snapPosition);
         app2.rank = encodeRank(SPLIT_POSITION_BOTTOM_OR_RIGHT, snapPosition);
         FolderInfo newAppPair = FolderInfo.createAppPair(app1, app2);
 
+        if (newAppPair.contents.size() != 2) {
+            // if app pair doesn't have exactly 2 members, log an error and do not create the app
+            // pair.
+            Log.wtf(TAG,
+                    "tried to save an app pair with " + newAppPair.contents.size() + " members");
+            return;
+        }
+
         IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
         MODEL_EXECUTOR.execute(() -> {
             newAppPair.contents.forEach(member -> {
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 79656c2..ec1eeb1 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -21,10 +21,10 @@
 import android.view.ViewGroup;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.NavigationMode;
 import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 
 public class LayoutUtils {
 
@@ -40,7 +40,7 @@
     }
 
     public static int getShelfTrackingDistance(Context context, DeviceProfile dp,
-            PagedOrientationHandler orientationHandler) {
+            RecentsPagedOrientationHandler orientationHandler) {
         // Track the bottom of the window.
         Rect taskSize = new Rect();
         LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize,
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index f6ad692..cba628b 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -53,6 +53,7 @@
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 
 import java.lang.annotation.Retention;
 import java.util.function.IntConsumer;
@@ -75,7 +76,8 @@
     @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
     public @interface SurfaceRotation {}
 
-    private PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+    private RecentsPagedOrientationHandler mOrientationHandler =
+            RecentsPagedOrientationHandler.PORTRAIT;
 
     private @SurfaceRotation int mTouchRotation = ROTATION_0;
     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
@@ -225,13 +227,13 @@
     private boolean updateHandler() {
         mRecentsActivityRotation = inferRecentsActivityRotation(mDisplayRotation);
         if (mRecentsActivityRotation == mTouchRotation || isRecentsActivityRotationAllowed()) {
-            mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+            mOrientationHandler = RecentsPagedOrientationHandler.PORTRAIT;
         } else if (mTouchRotation == ROTATION_90) {
-            mOrientationHandler = PagedOrientationHandler.LANDSCAPE;
+            mOrientationHandler = RecentsPagedOrientationHandler.LANDSCAPE;
         } else if (mTouchRotation == ROTATION_270) {
-            mOrientationHandler = PagedOrientationHandler.SEASCAPE;
+            mOrientationHandler = RecentsPagedOrientationHandler.SEASCAPE;
         } else {
-            mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+            mOrientationHandler = RecentsPagedOrientationHandler.PORTRAIT;
         }
         if (DEBUG) {
             Log.d(TAG, "current RecentsOrientedState: " + this);
@@ -413,7 +415,7 @@
         return scale;
     }
 
-    public PagedOrientationHandler getOrientationHandler() {
+    public RecentsPagedOrientationHandler getOrientationHandler() {
         return mOrientationHandler;
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 1abedb7..caffab1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -153,7 +153,17 @@
                     controller.finish(true /* toRecents */, null /* onFinishComplete */,
                             false /* sendUserLeaveHint */);
                 }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    mLauncher.getDragLayer().removeView(floatingTaskView);
+                    mController.getSplitAnimationController()
+                            .removeSplitInstructionsView(mLauncher);
+                    mController.resetState();
+                }
             });
+            anim.add(mController.getSplitAnimationController()
+                    .getShowSplitInstructionsAnim(mLauncher).buildAnim());
             anim.buildAnim().start();
         }
     };
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 348e4dc..9268511 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.ArrayMap;
+import android.view.DisplayCutout;
 import android.view.Surface;
 import android.view.WindowManager;
 import android.view.WindowMetrics;
@@ -74,4 +75,10 @@
         }
         return result;
     }
+
+    @Override
+    protected DisplayCutout rotateCutout(DisplayCutout original, int startWidth, int startHeight,
+            int fromRotation, int toRotation) {
+        return original.getRotated(startWidth, startHeight, fromRotation, toRotation);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 065a9c5..0bb6b23 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -40,6 +40,7 @@
 import android.view.RemoteAnimationTarget;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
@@ -344,6 +345,14 @@
      * Applies the target to the previously set parameters
      */
     public void apply(TransformParams params) {
+        apply(params, null);
+    }
+
+    /**
+     * Applies the target to the previously set parameters, optionally with an overridden
+     * surface transaction
+     */
+    public void apply(TransformParams params, @Nullable SurfaceTransaction surfaceTransaction) {
         if (mDp == null || mThumbnailPosition.isEmpty()) {
             return;
         }
@@ -404,7 +413,8 @@
         mTempRectF.roundOut(mTmpCropRect);
 
         params.setProgress(1f - fullScreenProgress);
-        params.applySurfaceParams(params.createSurfaceParams(this));
+        params.applySurfaceParams(surfaceTransaction == null
+                ? params.createSurfaceParams(this) : surfaceTransaction);
 
         if (!DEBUG) {
             return;
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index fba847f..32ef904 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -25,7 +25,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 
 public class ClearAllButton extends Button {
 
@@ -81,7 +81,8 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                getRecentsView().getPagedOrientationHandler();
         mSidePadding = orientationHandler.getClearAllSidePadding(getRecentsView(), mIsRtl);
     }
 
@@ -131,7 +132,8 @@
             return;
         }
 
-        PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                recentsView.getPagedOrientationHandler();
         float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
         if (orientationSize == 0) {
             return;
@@ -219,7 +221,8 @@
             return;
         }
 
-        PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                recentsView.getPagedOrientationHandler();
         orientationHandler.getPrimaryViewTranslate().set(this,
                 orientationHandler.getPrimaryValue(0f, getOriginalTranslationY())
                         + mNormalTranslationPrimary + getFullscreenTrans(
@@ -232,7 +235,8 @@
             return;
         }
 
-        PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                recentsView.getPagedOrientationHandler();
         orientationHandler.getSecondaryViewTranslate().set(this,
                 orientationHandler.getSecondaryValue(0f, getOriginalTranslationY()));
     }
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 937a728..840382d 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -50,8 +50,8 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.systemui.shared.recents.model.Task;
 
 import java.lang.annotation.Retention;
@@ -321,7 +321,7 @@
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
                 mTaskView.getThumbnail().getLayoutParams()).bottomMargin;
-        PagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
         Pair<Float, Float> translations = orientationHandler
                 .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
                         mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index 3a5873b..1c1e167 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -115,7 +115,7 @@
     }
 
     override fun draw(canvas: Canvas) {
-        if (launcher.deviceProfile.isLandscape) {
+        if (launcher.deviceProfile.isLeftRightSplit) {
             drawLeftRightSplit(canvas)
         } else {
             drawTopBottomSplit(canvas)
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index efc0a35..12a073f 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -43,9 +43,9 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.util.SplitAnimationTimings;
@@ -95,7 +95,7 @@
     private final StatefulActivity mActivity;
     private final boolean mIsRtl;
     private final FullscreenDrawParams mFullscreenParams;
-    private PagedOrientationHandler mOrientationHandler;
+    private RecentsPagedOrientationHandler mOrientationHandler;
     @SplitConfigurationOptions.StagePosition
     private int mStagePosition;
     private final Rect mTmpRect = new Rect();
@@ -220,7 +220,7 @@
         mOrientationHandler.setSecondaryScale(mSplitPlaceholderView.getIconView(), childScaleY);
     }
 
-    public void updateOrientationHandler(PagedOrientationHandler orientationHandler) {
+    public void updateOrientationHandler(RecentsPagedOrientationHandler orientationHandler) {
         mOrientationHandler = orientationHandler;
         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
     }
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 2ae64ff..66c67e7 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -7,11 +7,14 @@
 import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell;
 
 import android.content.Context;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewStub;
 
 import androidx.annotation.NonNull;
@@ -37,10 +40,11 @@
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
 
+import kotlin.Unit;
+
 import java.util.HashMap;
 import java.util.function.Consumer;
 
-import kotlin.Unit;
 
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
@@ -374,18 +378,7 @@
         }
         if (!enableOverviewIconMenu()) {
             updateIconPlacement();
-            return;
         }
-
-        if (getRecentsView() == null) {
-            return;
-        }
-
-        int iconMargins = getResources().getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_start_margin) * 2;
-        ((IconAppChipView) mIconView).setMaxWidth(mSnapshotView.getMeasuredWidth() - iconMargins);
-        ((IconAppChipView) mIconView2).setMaxWidth(mSnapshotView2.getMeasuredWidth() - iconMargins);
-        setOrientationState(getRecentsView().getPagedViewOrientedState());
     }
 
     @Override
@@ -395,8 +388,25 @@
 
     @Override
     public void setOrientationState(RecentsOrientedState orientationState) {
-        super.setOrientationState(orientationState);
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+        if (enableOverviewIconMenu() && mSplitBoundsConfig != null) {
+            ViewGroup.LayoutParams layoutParams = getLayoutParams();
+            Pair<Point, Point> groupedTaskViewSizes =
+                    orientationState.getOrientationHandler().getGroupedTaskViewSizes(
+                            deviceProfile,
+                            mSplitBoundsConfig,
+                            layoutParams.width,
+                            layoutParams.height
+                    );
+            int iconMargins = getResources().getDimensionPixelSize(
+                    R.dimen.task_thumbnail_icon_menu_start_margin) * 2;
+            ((IconAppChipView) mIconView).setMaxWidth(groupedTaskViewSizes.first.x - iconMargins);
+            ((IconAppChipView) mIconView2).setMaxWidth(groupedTaskViewSizes.second.x - iconMargins);
+        }
+        // setMaxWidth() needs to be called before mIconView.setIconOrientation which is called in
+        // the super below.
+        super.setOrientationState(orientationState);
+
         boolean isGridTask = deviceProfile.isTablet && !isFocusedTask();
         mIconView2.setIconOrientation(orientationState, isGridTask);
         updateIconPlacement();
@@ -412,10 +422,27 @@
         int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
 
-        getPagedOrientationHandler().setSplitIconParams(mIconView.asView(), mIconView2.asView(),
-                taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(),
-                getMeasuredHeight(), getMeasuredWidth(), isRtl, deviceProfile,
-                mSplitBoundsConfig);
+        if (enableOverviewIconMenu()) {
+            ViewGroup.LayoutParams layoutParams = getLayoutParams();
+            Pair<Point, Point> groupedTaskViewSizes =
+                    getPagedOrientationHandler()
+                            .getGroupedTaskViewSizes(
+                                    deviceProfile,
+                                    mSplitBoundsConfig,
+                                    layoutParams.width,
+                                    layoutParams.height
+                            );
+
+            getPagedOrientationHandler().setSplitIconParams(mIconView.asView(), mIconView2.asView(),
+                    taskIconHeight, groupedTaskViewSizes.first.x, groupedTaskViewSizes.first.y,
+                    getLayoutParams().height, getLayoutParams().width, isRtl, deviceProfile,
+                    mSplitBoundsConfig);
+        } else {
+            getPagedOrientationHandler().setSplitIconParams(mIconView.asView(), mIconView2.asView(),
+                    taskIconHeight, mSnapshotView.getMeasuredWidth(),
+                    mSnapshotView.getMeasuredHeight(), getMeasuredHeight(), getMeasuredWidth(),
+                    isRtl, deviceProfile, mSplitBoundsConfig);
+        }
     }
 
     private void updateSecondaryDwbPlacement() {
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
index ee09c4d..3347665 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -39,9 +39,9 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.RecentsOrientedState;
 
 /**
@@ -52,9 +52,10 @@
     private static final int MENU_BACKGROUND_REVEAL_DURATION = 417;
     private static final int MENU_BACKGROUND_HIDE_DURATION = 333;
 
-    private static final int NUM_ALPHA_CHANNELS = 2;
+    private static final int NUM_ALPHA_CHANNELS = 3;
     private static final int INDEX_CONTENT_ALPHA = 0;
     private static final int INDEX_COLOR_FILTER_ALPHA = 1;
+    private static final int INDEX_MODAL_ALPHA = 2;
 
     private final MultiValueAlpha mMultiValueAlpha;
 
@@ -86,6 +87,7 @@
     private final int mMinIconBackgroundHeight;
     private final int mMaxIconBackgroundCornerRadius;
     private final float mMinIconBackgroundCornerRadius;
+    private AnimatorSet mAnimator;
 
     private int mMaxWidth = Integer.MAX_VALUE;
 
@@ -211,7 +213,8 @@
 
     @Override
     public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
-        PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                orientationState.getOrientationHandler();
         boolean isRtl = isLayoutRtl();
         DeviceProfile deviceProfile =
                 ActivityContext.lookupContext(getContext()).getDeviceProfile();
@@ -305,6 +308,11 @@
     }
 
     @Override
+    public void setModalAlpha(float alpha) {
+        mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
+    }
+
+    @Override
     public int getDrawableWidth() {
         return mIconView == null ? 0 : mIconView.getDrawableWidth();
     }
@@ -315,11 +323,13 @@
     }
 
     protected void revealAnim(boolean isRevealing) {
+        cancelInProgressAnimations();
+
         if (isRevealing) {
             boolean isRtl = isLayoutRtl();
             bringToFront();
             ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).start();
-            AnimatorSet anim = new AnimatorSet();
+            mAnimator = new AnimatorSet();
             float backgroundScaleY = mMaxIconBackgroundHeight / (float) mMinIconBackgroundHeight;
             float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
                     mMaxIconBackgroundCornerRadius);
@@ -340,7 +350,7 @@
                             mIconTextMaxWidth + maxCornerSize);
                 }
             });
-            anim.playTogether(
+            mAnimator.playTogether(
                     expandedTextRevealAnim,
                     ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y,
                             backgroundScaleY),
@@ -366,9 +376,9 @@
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
                     ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X,
                             isRtl ? -arrowTranslationX : arrowTranslationX));
-            anim.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
-            anim.setInterpolator(EMPHASIZED);
-            anim.start();
+            mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
+            mAnimator.setInterpolator(EMPHASIZED);
+            mAnimator.start();
         } else {
             ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reverse();
             float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
@@ -385,8 +395,8 @@
                             mIconTextExpandedView.getHeight() / 2f, 0);
                 }
             });
-            AnimatorSet anim = new AnimatorSet();
-            anim.playTogether(
+            mAnimator = new AnimatorSet();
+            mAnimator.playTogether(
                     expandedTextClipAnim,
                     ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_X, 1),
                     ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y, 1),
@@ -402,9 +412,9 @@
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
                     ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0));
-            anim.setDuration(MENU_BACKGROUND_HIDE_DURATION);
-            anim.setInterpolator(EMPHASIZED);
-            anim.start();
+            mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
+            mAnimator.setInterpolator(EMPHASIZED);
+            mAnimator.start();
         }
     }
 
@@ -424,6 +434,16 @@
         mIconTextExpandedView.setAlpha(0);
         mIconArrowView.setTranslationX(0);
         ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reset();
+        mAnimator = null;
+    }
+
+    private void cancelInProgressAnimations() {
+        // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
+        // expecting to be mutable and will cause a crash if they are re-used.
+        if (mAnimator != null && mAnimator.isStarted()) {
+            mAnimator.cancel();
+            mAnimator = null;
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
index a4bda7f..4d33fda 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -30,8 +30,8 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.RecentsOrientedState;
 
 /**
@@ -149,6 +149,11 @@
     }
 
     @Override
+    public void setModalAlpha(float alpha) {
+        setAlpha(alpha);
+    }
+
+    @Override
     public void setAlpha(float alpha) {
         super.setAlpha(alpha);
         if (alpha > 0) {
@@ -173,7 +178,8 @@
 
     @Override
     public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
-        PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                orientationState.getOrientationHandler();
         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
         DeviceProfile deviceProfile =
                 ActivityContext.lookupContext(getContext()).getDeviceProfile();
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 6b3484d..fecbf08 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -156,7 +156,6 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.OverScroll;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -187,6 +186,7 @@
 import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AnimUtils;
@@ -542,6 +542,9 @@
     private int mOverScrollShift = 0;
     private long mScrollLastHapticTimestamp;
 
+    private int mKeyboardTaskFocusSnapAnimationDuration;
+    private int mKeyboardTaskFocusIndex = INVALID_PAGE;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -788,7 +791,8 @@
         mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop,
                 5 /* max size */, 1 /* initial size */);
 
-        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
+        setOrientationHandler(mOrientationState.getOrientationHandler());
+        mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources());
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mSplitPlaceholderSize = getResources().getDimensionPixelSize(
                 R.dimen.split_placeholder_size);
@@ -812,7 +816,6 @@
                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
         setWillNotDraw(false);
         updateEmptyMessage();
-        mOrientationHandler = mOrientationState.getOrientationHandler();
 
         mTaskOverlayFactory = Overrides.getObject(
                 TaskOverlayFactory.class,
@@ -918,9 +921,9 @@
         if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) {
             final int restoreCount = canvas.save();
 
-            int primarySize = mOrientationHandler.getPrimaryValue(getWidth(), getHeight());
+            int primarySize = getPagedOrientationHandler().getPrimaryValue(getWidth(), getHeight());
             int scroll = OverScroll.dampedScroll(getUndampedOverScrollShift(), primarySize);
-            mOrientationHandler.setPrimary(canvas, CANVAS_TRANSLATE, scroll);
+            getPagedOrientationHandler().setPrimary(canvas, CANVAS_TRANSLATE, scroll);
 
             if (mOverScrollShift != scroll) {
                 mOverScrollShift = scroll;
@@ -944,8 +947,8 @@
     private float getUndampedOverScrollShift() {
         final int width = getWidth();
         final int height = getHeight();
-        int primarySize = mOrientationHandler.getPrimaryValue(width, height);
-        int secondarySize = mOrientationHandler.getSecondaryValue(width, height);
+        int primarySize = getPagedOrientationHandler().getPrimaryValue(width, height);
+        int secondarySize = getPagedOrientationHandler().getSecondaryValue(width, height);
 
         float effectiveShift = 0;
         if (!mEdgeGlowLeft.isFinished()) {
@@ -1270,8 +1273,8 @@
 
     public boolean isTaskViewVisible(TaskView tv) {
         if (showAsGrid()) {
-            int screenStart = mOrientationHandler.getPrimaryScroll(this);
-            int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
+            int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
+            int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
             return isTaskViewWithinBounds(tv, screenStart, screenEnd);
         } else {
             // For now, just check if it's the active task or an adjacent task
@@ -1281,8 +1284,8 @@
 
     public boolean isTaskViewFullyVisible(TaskView tv) {
         if (showAsGrid()) {
-            int screenStart = mOrientationHandler.getPrimaryScroll(this);
-            int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
+            int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
+            int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
             return isTaskViewFullyWithinBounds(tv, screenStart, screenEnd);
         } else {
             // For now, just check if it's the active task
@@ -1307,9 +1310,9 @@
 
     private int getSnapToLastTaskScrollDiff() {
         // Snap to a position where ClearAll is just invisible.
-        int screenStart = mOrientationHandler.getPrimaryScroll(this);
+        int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
         int clearAllScroll = getScrollForPage(indexOfChild(mClearAllButton));
-        int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton);
+        int clearAllWidth = getPagedOrientationHandler().getPrimarySize(mClearAllButton);
         int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
         return screenStart - lastTaskScroll;
     }
@@ -1320,20 +1323,20 @@
     }
 
     private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
-        int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
-                showAsGrid());
-        int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
-                showAsFullscreen()));
+        int taskStart = getPagedOrientationHandler().getChildStart(tv)
+                + (int) tv.getOffsetAdjustment(showAsGrid());
+        int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(tv)
+                * tv.getSizeAdjustment(showAsFullscreen()));
         int taskEnd = taskStart + taskSize;
         return (taskStart >= start && taskStart <= end) || (taskEnd >= start
                 && taskEnd <= end);
     }
 
     private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
-        int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
-                showAsGrid());
-        int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
-                showAsFullscreen()));
+        int taskStart = getPagedOrientationHandler().getChildStart(tv)
+                + (int) tv.getOffsetAdjustment(showAsGrid());
+        int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(tv)
+                * tv.getSizeAdjustment(showAsFullscreen()));
         int taskEnd = taskStart + taskSize;
         return taskStart >= start && taskEnd <= end;
     }
@@ -1628,7 +1631,7 @@
             return;
         }
 
-        int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+        int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
         int currentPageScroll = getScrollForPage(mCurrentPage);
         mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
 
@@ -1704,6 +1707,7 @@
         // Removing views sets the currentPage to 0, so we save this and restore it after
         // the new set of views are added
         int previousCurrentPage = mCurrentPage;
+        int previousFocusedPage = indexOfChild(getFocusedChild());
         removeAllViews();
 
         // If we are entering Overview as a result of initiating a split from somewhere else
@@ -1807,9 +1811,7 @@
         mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview()
                 ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID;
         updateTaskSize();
-        if (newFocusedTaskView != null) {
-            newFocusedTaskView.setOrientationState(mOrientationState);
-        }
+        updateChildTaskOrientations();
 
         TaskView newRunningTaskView = null;
         if (hasAnyValidTaskIds(runningTaskId)) {
@@ -1833,6 +1835,8 @@
                     targetPage = indexOfChild(currentTaskView);
                 }
             }
+        } else if (previousFocusedPage != INVALID_PAGE) {
+            targetPage = previousFocusedPage;
         } else {
             // Set the current page to the running task, but not if settling on new task.
             if (hasAnyValidTaskIds(runningTaskId)) {
@@ -2021,20 +2025,20 @@
 
     private void updateOrientationHandler(boolean forceRecreateDragLayerControllers) {
         // Handle orientation changes.
-        PagedOrientationHandler oldOrientationHandler = mOrientationHandler;
-        mOrientationHandler = mOrientationState.getOrientationHandler();
+        RecentsPagedOrientationHandler oldOrientationHandler = getPagedOrientationHandler();
+        setOrientationHandler(mOrientationState.getOrientationHandler());
 
-        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
+        mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources());
         setLayoutDirection(mIsRtl
                 ? View.LAYOUT_DIRECTION_RTL
                 : View.LAYOUT_DIRECTION_LTR);
         mClearAllButton.setLayoutDirection(mIsRtl
                 ? View.LAYOUT_DIRECTION_LTR
                 : View.LAYOUT_DIRECTION_RTL);
-        mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
+        mClearAllButton.setRotation(getPagedOrientationHandler().getDegreesRotated());
 
         if (forceRecreateDragLayerControllers
-                || !mOrientationHandler.equals(oldOrientationHandler)) {
+                || !getPagedOrientationHandler().equals(oldOrientationHandler)) {
             // Changed orientations, update controllers so they intercept accordingly.
             mActivity.getDragLayer().recreateControllers();
             onOrientationChanged();
@@ -2081,7 +2085,7 @@
         mSizeStrategy.calculateGridSize(mActivity.getDeviceProfile(), mActivity,
                 mLastComputedGridSize);
         mSizeStrategy.calculateGridTaskSize(mActivity, mActivity.getDeviceProfile(),
-                mLastComputedGridTaskSize, mOrientationHandler);
+                mLastComputedGridTaskSize, getPagedOrientationHandler());
         if (isDesktopModeSupported()) {
             mSizeStrategy.calculateDesktopTaskSize(mActivity, mActivity.getDeviceProfile(),
                     mLastComputedDesktopTaskSize);
@@ -2136,7 +2140,7 @@
 
     public void getTaskSize(Rect outRect) {
         mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
-                mOrientationHandler);
+                getPagedOrientationHandler());
         mLastComputedTaskSize.set(outRect);
     }
 
@@ -2159,7 +2163,7 @@
 
     private Rect getTaskBounds(TaskView taskView) {
         int selectedPage = indexOfChild(taskView);
-        int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+        int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
         int selectedPageScroll = getScrollForPage(selectedPage);
         boolean isTopRow = taskView != null && mTopRowIdSet.contains(taskView.getTaskViewId());
         Rect outRect = new Rect(mLastComputedTaskSize);
@@ -2187,7 +2191,7 @@
     /** Gets the task size for modal state. */
     public void getModalTaskSize(Rect outRect) {
         mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
-                mOrientationHandler);
+                getPagedOrientationHandler());
     }
 
     @Override
@@ -2243,7 +2247,7 @@
         if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
             return;
         }
-        int scroll = mOrientationHandler.getPrimaryScroll(this);
+        int scroll = getPagedOrientationHandler().getPrimaryScroll(this);
         mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled);
 
         // Clear all button alpha was set by the previous line.
@@ -2293,8 +2297,8 @@
         int visibleStart = 0;
         int visibleEnd = 0;
         if (showAsGrid()) {
-            int screenStart = mOrientationHandler.getPrimaryScroll(this);
-            int pageOrientedSize = mOrientationHandler.getMeasuredSize(this);
+            int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
+            int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
             // For GRID_ONLY_OVERVIEW, use +/- 1 task column as visible area for preloading
             // adjacent thumbnails, otherwise use +/-50% screen width
             int extraWidth = enableGridOnlyOverview() ? getLastComputedTaskSize().width()
@@ -2914,7 +2918,7 @@
         int focusedTaskShift = 0;
         int focusedTaskWidthAndSpacing = 0;
         int snappedTaskRowWidth = 0;
-        int snappedPage = getNextPage();
+        int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
         TaskView snappedTaskView = getTaskViewAt(snappedPage);
         TaskView homeTaskView = getHomeTaskView();
         TaskView nextFocusedTaskView = null;
@@ -3232,8 +3236,8 @@
                 clampToProgress(isOnGridBottomRow(taskView) ? ACCELERATE : FINAL_FRAME, 0, 0.5f));
         FloatProperty<TaskView> secondaryViewTranslate =
                 taskView.getSecondaryDismissTranslationProperty();
-        int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
-        int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
+        int secondaryTaskDimension = getPagedOrientationHandler().getSecondaryDimension(taskView);
+        int verticalFactor = getPagedOrientationHandler().getSecondaryTranslationDirectionFactor();
 
         ResourceProvider rp = DynamicResource.provider(mActivity);
         SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
@@ -3248,7 +3252,7 @@
                 if (!mEnableDrawingLiveTile) return;
                 runActionOnRemoteHandles(
                         remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
-                                .taskSecondaryTranslation.value = mOrientationHandler
+                                .taskSecondaryTranslation.value = getPagedOrientationHandler()
                                 .getSecondaryValue(taskView.getTranslationX(),
                                         taskView.getTranslationY()
                                 ));
@@ -3262,7 +3266,7 @@
      * and then animates it into the split position that was desired
      */
     private void createInitialSplitSelectAnimation(PendingAnimation anim) {
-        mOrientationHandler.getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
+        getPagedOrientationHandler().getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
                 mSplitPlaceholderInset, mActivity.getDeviceProfile(),
                 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
         SplitAnimationTimings timings =
@@ -3491,7 +3495,7 @@
                 // beyond that, we'll need to snap to last task instead.
                 TaskView lastTask = getLastGridTaskView();
                 if (lastTask != null) {
-                    int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+                    int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
                     int lastTaskScroll = getScrollForPage(indexOfChild(lastTask));
                     if ((mIsRtl && primaryScroll < lastTaskScroll)
                             || (!mIsRtl && primaryScroll > lastTaskScroll)) {
@@ -3597,7 +3601,7 @@
                 if (scrollDiff != 0) {
                     FloatProperty translationProperty = child instanceof TaskView
                             ? ((TaskView) child).getPrimaryDismissTranslationProperty()
-                            : mOrientationHandler.getPrimaryViewTranslate();
+                            : getPagedOrientationHandler().getPrimaryViewTranslate();
 
                     float additionalDismissDuration =
                             ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
@@ -3633,7 +3637,7 @@
                                     remoteTargetHandle ->
                                             remoteTargetHandle.getTaskViewSimulator()
                                                     .taskPrimaryTranslation.value =
-                                                    mOrientationHandler.getPrimaryValue(
+                                                    getPagedOrientationHandler().getPrimaryValue(
                                                             child.getTranslationX(),
                                                             child.getTranslationY()
                                                     ));
@@ -3709,7 +3713,7 @@
                     if (isStagingFocusedTask) {
                         // Moves less if focused task is not in scroll position.
                         int focusedTaskScroll = getScrollForPage(dismissedIndex);
-                        int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+                        int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
                         int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll;
                         primaryTranslation +=
                                 mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff;
@@ -3837,7 +3841,7 @@
                             }
 
                             if (calculateScrollDiff) {
-                                int primaryScroll = mOrientationHandler.getPrimaryScroll(
+                                int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(
                                         RecentsView.this);
                                 int currentPageScroll = getScrollForPage(mCurrentPage);
                                 mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
@@ -3878,9 +3882,9 @@
                                 TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex);
 
                                 boolean shouldRebalance;
-                                int screenStart = mOrientationHandler.getPrimaryScroll(
+                                int screenStart = getPagedOrientationHandler().getPrimaryScroll(
                                         RecentsView.this);
-                                int taskStart = mOrientationHandler.getChildStart(taskView)
+                                int taskStart = getPagedOrientationHandler().getChildStart(taskView)
                                         + (int) taskView.getOffsetAdjustment(/*gridEnabled=*/ true);
 
                                 // Rebalance only if there is a maximum gap between the task and the
@@ -3889,10 +3893,11 @@
                                 if (mIsRtl) {
                                     shouldRebalance = taskStart <= screenStart + mPageSpacing;
                                 } else {
-                                    int screenEnd =
-                                            screenStart + mOrientationHandler.getMeasuredSize(
-                                                    RecentsView.this);
-                                    int taskSize = (int) (mOrientationHandler.getMeasuredSize(
+                                    int screenEnd = screenStart
+                                            + getPagedOrientationHandler().getMeasuredSize(
+                                            RecentsView.this);
+                                    int taskSize = (int) (
+                                            getPagedOrientationHandler().getMeasuredSize(
                                             taskView) * taskView
                                             .getSizeAdjustment(/*fullscreenEnabled=*/false));
                                     int taskEnd = taskStart + taskSize;
@@ -4287,8 +4292,8 @@
         return mOrientationState;
     }
 
-    public PagedOrientationHandler getPagedOrientationHandler() {
-        return mOrientationHandler;
+    public RecentsPagedOrientationHandler getPagedOrientationHandler() {
+        return (RecentsPagedOrientationHandler) super.getPagedOrientationHandler();
     }
 
     @Nullable
@@ -4466,7 +4471,7 @@
             View child = getChildAt(i);
             FloatProperty translationPropertyX = child instanceof TaskView
                     ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
-                    : mOrientationHandler.getPrimaryViewTranslate();
+                    : getPagedOrientationHandler().getPrimaryViewTranslate();
             translationPropertyX.set(child, totalTranslationX);
             if (mEnableDrawingLiveTile && i == getRunningTaskIndex()) {
                 runActionOnRemoteHandles(
@@ -4504,8 +4509,8 @@
                     mIsRtl ? outRect.right : outRect.left, outRect.top);
             mTempMatrix.mapRect(outRect);
         }
-        outRect.offset(mOrientationHandler.getPrimaryValue(-midPointScroll, 0),
-                mOrientationHandler.getSecondaryValue(-midPointScroll, 0));
+        outRect.offset(getPagedOrientationHandler().getPrimaryValue(-midPointScroll, 0),
+                getPagedOrientationHandler().getSecondaryValue(-midPointScroll, 0));
     }
 
     /**
@@ -4530,14 +4535,15 @@
             // to reach offscreen. Offset the task position to the task's starting point, and offset
             // by current page's scroll diff.
             int midpointScroll = getScrollForPage(midpointIndex)
-                    + mOrientationHandler.getPrimaryScroll(this) - getScrollForPage(mCurrentPage);
+                    + getPagedOrientationHandler().getPrimaryScroll(this)
+                    - getScrollForPage(mCurrentPage);
 
             getPersistentChildPosition(midpointIndex, midpointScroll, taskPosition);
-            float midpointStart = mOrientationHandler.getStart(taskPosition);
+            float midpointStart = getPagedOrientationHandler().getStart(taskPosition);
 
             getPersistentChildPosition(childIndex, midpointScroll, taskPosition);
             // Assume child does not overlap with midPointChild.
-            isStartShift = mOrientationHandler.getStart(taskPosition) < midpointStart;
+            isStartShift = getPagedOrientationHandler().getStart(taskPosition) < midpointStart;
         } else {
             // Position the task at scroll position.
             getPersistentChildPosition(childIndex, getScrollForPage(childIndex), taskPosition);
@@ -4551,27 +4557,29 @@
         // desired position, and adjust the computed distance accordingly.
         float distanceToOffscreen;
         if (isStartShift) {
-            float desiredStart = -mOrientationHandler.getPrimarySize(taskPosition);
-            distanceToOffscreen = -mOrientationHandler.getEnd(taskPosition);
+            float desiredStart = -getPagedOrientationHandler().getPrimarySize(taskPosition);
+            distanceToOffscreen = -getPagedOrientationHandler().getEnd(taskPosition);
             if (mLastComputedTaskStartPushOutDistance == null) {
                 taskPosition.offsetTo(
-                        mOrientationHandler.getPrimaryValue(desiredStart, 0f),
-                        mOrientationHandler.getSecondaryValue(desiredStart, 0f));
+                        getPagedOrientationHandler().getPrimaryValue(desiredStart, 0f),
+                        getPagedOrientationHandler().getSecondaryValue(desiredStart, 0f));
                 getMatrix().mapRect(taskPosition);
-                mLastComputedTaskStartPushOutDistance = mOrientationHandler.getEnd(taskPosition)
-                        / mOrientationHandler.getPrimaryScale(this);
+                mLastComputedTaskStartPushOutDistance = getPagedOrientationHandler().getEnd(
+                        taskPosition) / getPagedOrientationHandler().getPrimaryScale(this);
             }
             distanceToOffscreen -= mLastComputedTaskStartPushOutDistance;
         } else {
-            float desiredStart = mOrientationHandler.getPrimarySize(this);
-            distanceToOffscreen = desiredStart - mOrientationHandler.getStart(taskPosition);
+            float desiredStart = getPagedOrientationHandler().getPrimarySize(this);
+            distanceToOffscreen = desiredStart - getPagedOrientationHandler().getStart(
+                    taskPosition);
             if (mLastComputedTaskEndPushOutDistance == null) {
                 taskPosition.offsetTo(
-                        mOrientationHandler.getPrimaryValue(desiredStart, 0f),
-                        mOrientationHandler.getSecondaryValue(desiredStart, 0f));
+                        getPagedOrientationHandler().getPrimaryValue(desiredStart, 0f),
+                        getPagedOrientationHandler().getSecondaryValue(desiredStart, 0f));
                 getMatrix().mapRect(taskPosition);
-                mLastComputedTaskEndPushOutDistance = (mOrientationHandler.getStart(taskPosition)
-                        - desiredStart) / mOrientationHandler.getPrimaryScale(this);
+                mLastComputedTaskEndPushOutDistance = (getPagedOrientationHandler().getStart(
+                        taskPosition) - desiredStart)
+                        / getPagedOrientationHandler().getPrimaryScale(this);
             }
             distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
         }
@@ -4660,7 +4668,7 @@
      * of split invocation as such.
      */
     public void initiateSplitSelect(TaskView taskView) {
-        int defaultSplitPosition = mOrientationHandler
+        int defaultSplitPosition = getPagedOrientationHandler()
                 .getDefaultSplitPosition(mActivity.getDeviceProfile());
         initiateSplitSelect(taskView, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT);
     }
@@ -4803,7 +4811,7 @@
 
         int halfDividerSize = getResources()
                 .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
-        mOrientationHandler.getFinalSplitPlaceholderBounds(halfDividerSize,
+        getPagedOrientationHandler().getFinalSplitPlaceholderBounds(halfDividerSize,
                 mActivity.getDeviceProfile(),
                 mSplitSelectStateController.getActiveSplitStagePosition(), firstTaskEndingBounds,
                 secondTaskEndingBounds);
@@ -4920,7 +4928,7 @@
      */
     public float getSplitSelectTranslation() {
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-        PagedOrientationHandler orientationHandler = getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
         int splitPosition = getSplitSelectController().getActiveSplitStagePosition();
         int splitPlaceholderSize =
                 mActivity.getResources().getDimensionPixelSize(R.dimen.split_placeholder_size);
@@ -4945,16 +4953,16 @@
     }
 
     protected void onRotateInSplitSelectionState() {
-        mOrientationHandler.getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
+        getPagedOrientationHandler().getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
                 mSplitPlaceholderInset, mActivity.getDeviceProfile(),
                 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
         mTempRectF.set(mTempRect);
         FloatingTaskView firstFloatingTaskView =
                 mSplitSelectStateController.getFirstFloatingTaskView();
-        firstFloatingTaskView.updateOrientationHandler(mOrientationHandler);
+        firstFloatingTaskView.updateOrientationHandler(getPagedOrientationHandler());
         firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
 
-        PagedOrientationHandler orientationHandler = getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
         Pair<FloatProperty, FloatProperty> taskViewsFloat =
                 orientationHandler.getSplitSelectTaskOffset(
                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
@@ -5059,7 +5067,7 @@
             float displacementX = tv.getWidth() * (toScale - 1f);
             float primaryTranslation = mIsRtl ? -displacementX : displacementX;
             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
-                    mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation));
+                    getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation));
             int runningTaskIndex = getRunningTaskIndex();
             if (runningTaskIndex != -1 && runningTaskIndex != taskIndex
                     && getRemoteTargetHandles() != null) {
@@ -5075,7 +5083,7 @@
             if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
                 PropertyValuesHolder[] properties = new PropertyValuesHolder[3];
                 properties[0] = PropertyValuesHolder.ofFloat(
-                        mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation);
+                        getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation);
                 properties[1] = PropertyValuesHolder.ofFloat(View.SCALE_X, 1);
                 properties[2] = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1);
 
@@ -5495,8 +5503,8 @@
         // Align ClearAllButton to the left (RTL) or right (non-RTL), which is different from other
         // TaskViews. This must be called after laying out ClearAllButton.
         if (layoutChildren) {
-            int clearAllWidthDiff = mOrientationHandler.getPrimaryValue(mTaskWidth, mTaskHeight)
-                    - mOrientationHandler.getPrimarySize(mClearAllButton);
+            int clearAllWidthDiff = getPagedOrientationHandler().getPrimaryValue(mTaskWidth,
+                    mTaskHeight) - getPagedOrientationHandler().getPrimarySize(mClearAllButton);
             mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff);
         }
 
@@ -5504,7 +5512,7 @@
 
         int clearAllIndex = indexOfChild(mClearAllButton);
         int clearAllScroll = 0;
-        int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton);
+        int clearAllWidth = getPagedOrientationHandler().getPrimarySize(mClearAllButton);
         if (clearAllIndex != -1 && clearAllIndex < outPageScrolls.length) {
             float scrollDiff = mClearAllButton.getScrollAdjustment(showAsFullscreen, showAsGrid);
             clearAllScroll = newPageScrolls[clearAllIndex] + (int) scrollDiff;
@@ -5573,6 +5581,19 @@
     }
 
     /**
+     * Returns how many pixels the running task is offset on the currently laid out dominant axis
+     * specifically during a Keyboard task focus.
+     */
+    public int getScrollOffsetForKeyboardTaskFocus() {
+        if (!isKeyboardTaskFocusPending()) {
+            return getScrollOffset(getRunningTaskIndex());
+        }
+        return getPagedOrientationHandler().getPrimaryScroll(this)
+                - getScrollForPage(mKeyboardTaskFocusIndex)
+                + getScrollOffset(getRunningTaskIndex());
+    }
+
+    /**
      * Sets whether or not we should clamp the scroll offset.
      * This is used to avoid x-axis movement when swiping up transient taskbar.
      * Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
@@ -5605,15 +5626,15 @@
         if (pageIndex == -1) {
             return 0;
         }
-
-        int overScrollShift = getOverScrollShift();
-        if (mAdjacentPageHorizontalOffset > 0) {
-            // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so
-            // that the page can move freely given there's no visual indication why it shouldn't.
-            overScrollShift = (int) Utilities.mapRange(mAdjacentPageHorizontalOffset,
-                    overScrollShift, getUndampedOverScrollShift());
-        }
-        return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this)
+        // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
+        // the page can move freely given there's no visual indication why it shouldn't.
+        int overScrollShift = mAdjacentPageHorizontalOffset > 0
+                        ? (int) Utilities.mapRange(
+                                mAdjacentPageHorizontalOffset,
+                                getOverScrollShift(),
+                                getUndampedOverScrollShift())
+                        : getOverScrollShift();
+        return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
                 + overScrollShift + getOffsetFromScrollPosition(pageIndex);
     }
 
@@ -5682,7 +5703,7 @@
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
         float degreesRotated;
         if (navbarRotation == 0) {
-            degreesRotated = mOrientationHandler.getDegreesRotated();
+            degreesRotated = getPagedOrientationHandler().getDegreesRotated();
         } else {
             degreesRotated = -navbarRotation;
         }
@@ -6021,11 +6042,50 @@
         dispatchScrollChanged();
     }
 
+    /**
+     * Prepares this RecentsView to scroll properly for an upcoming child view focus request from
+     * keyboard quick switching
+     */
+    public void setKeyboardTaskFocusIndex(int taskIndex) {
+        mKeyboardTaskFocusIndex = taskIndex;
+    }
+
+    /** Returns whether this RecentsView will be scrolling to a child view for a focus request */
+    public boolean isKeyboardTaskFocusPending() {
+        return mKeyboardTaskFocusIndex != INVALID_PAGE;
+    }
+
+    private boolean isKeyboardTaskFocusPendingForChild(View child) {
+        return isKeyboardTaskFocusPending() && mKeyboardTaskFocusIndex == indexOfChild(child);
+    }
+
     @Override
-    protected boolean shouldHandleRequestChildFocus() {
-        // If we are already scrolling to a task view, then the focus request has already been
-        // handled
-        return mScroller.isFinished();
+    protected int getSnapAnimationDuration() {
+        return isKeyboardTaskFocusPending()
+                ? mKeyboardTaskFocusSnapAnimationDuration : super.getSnapAnimationDuration();
+    }
+
+    @Override
+    protected void onVelocityValuesUpdated() {
+        super.onVelocityValuesUpdated();
+        mKeyboardTaskFocusSnapAnimationDuration =
+                getResources().getInteger(R.integer.config_keyboardTaskFocusSnapAnimationDuration);
+    }
+
+    @Override
+    protected boolean shouldHandleRequestChildFocus(View child) {
+        // If we are already scrolling to a task view and we aren't focusing to this child from
+        // keyboard quick switch, then the focus request has already been handled
+        return mScroller.isFinished() || isKeyboardTaskFocusPendingForChild(child);
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        if (isKeyboardTaskFocusPendingForChild(child)) {
+            updateGridProperties();
+            updateScrollSynchronously();
+        }
+        super.requestChildFocus(child, focused);
     }
 
     private void dispatchScrollChanged() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 77033b2..cf50835 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -24,6 +24,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Outline;
 import android.graphics.Rect;
@@ -48,10 +49,10 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskUtils;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.TaskCornerRadius;
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
 
@@ -69,6 +70,8 @@
     private TextView mTaskName;
     @Nullable
     private AnimatorSet mOpenCloseAnimator;
+    @Nullable
+    private ValueAnimator mRevealAnimator;
     @Nullable private Runnable mOnClosingStartCallback;
     private TaskView mTaskView;
     private TaskIdAttributeContainer mTaskContainer;
@@ -215,7 +218,8 @@
 
     private void orientAroundTaskView(TaskIdAttributeContainer taskContainer) {
         RecentsView recentsView = mActivity.getOverviewPanel();
-        PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+        RecentsPagedOrientationHandler orientationHandler =
+                recentsView.getPagedOrientationHandler();
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
 
         // Get Position
@@ -289,13 +293,18 @@
 
     private void animateOpenOrClosed(boolean closing) {
         if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
-            mOpenCloseAnimator.end();
+            mOpenCloseAnimator.cancel();
         }
         mOpenCloseAnimator = new AnimatorSet();
-
-        final Animator revealAnimator = createOpenCloseOutlineProvider()
-                .createRevealAnimator(this, closing);
-        revealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
+        // If we're opening, we just start from the beginning as a new `TaskMenuView` is created
+        // each time we do the open animation so there will never be a partial value here.
+        float revealAnimationStartProgress = 0f;
+        if (closing && mRevealAnimator != null) {
+            revealAnimationStartProgress = 1f - mRevealAnimator.getAnimatedFraction();
+        }
+        mRevealAnimator = createOpenCloseOutlineProvider()
+                .createRevealAnimator(this, closing, revealAnimationStartProgress);
+        mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
                 : Interpolators.DECELERATE);
 
         if (enableOverviewIconMenu()) {
@@ -348,7 +357,7 @@
             mOpenCloseAnimator.playTogether(translationXAnim, menuTranslationXAnim);
         }
 
-        mOpenCloseAnimator.playTogether(revealAnimator,
+        mOpenCloseAnimator.playTogether(mRevealAnimator,
                 ObjectAnimator.ofFloat(
                         mTaskContainer.getThumbnailView(), DIM_ALPHA,
                         closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
@@ -377,6 +386,7 @@
         mIsOpen = false;
         resetOverviewIconMenu();
         mActivity.getDragLayer().removeView(this);
+        mRevealAnimator = null;
     }
 
     private void resetOverviewIconMenu() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index dff0580..077247b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -51,11 +51,11 @@
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -513,7 +513,7 @@
             return false;
         }
 
-        if (recents.getPagedOrientationHandler() == PagedOrientationHandler.PORTRAIT) {
+        if (recents.getPagedOrientationHandler() == RecentsPagedOrientationHandler.PORTRAIT) {
             int currentRotation = recents.getPagedViewOrientedState().getRecentsActivityRotation();
             return (currentRotation - mThumbnailData.rotation) % 2 != 0;
         } else {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index f2c0286..5057c38 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -87,7 +87,6 @@
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.RunnableList;
@@ -104,6 +103,7 @@
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.BorderAnimator;
 import com.android.quickstep.util.CancellableTask;
@@ -116,6 +116,8 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 
+import kotlin.Unit;
+
 import java.lang.annotation.Retention;
 import java.util.Arrays;
 import java.util.Collections;
@@ -124,8 +126,6 @@
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
-import kotlin.Unit;
-
 /**
  * A task in the Recents view.
  */
@@ -632,7 +632,7 @@
             return;
         }
         mModalness = modalness;
-        mIconView.setContentAlpha(1 - modalness);
+        mIconView.setModalAlpha(1 - modalness);
         mDigitalWellBeingToast.updateBannerOffset(modalness);
     }
 
@@ -1137,9 +1137,8 @@
         DeviceProfile dp = mActivity.getDeviceProfile();
         if (enableOverviewIconMenu() && iconView instanceof IconAppChipView) {
             ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ true);
-            return TaskMenuView.showForTask(menuContainer, () -> {
-                ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false);
-            });
+            return TaskMenuView.showForTask(menuContainer,
+                    () -> ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false));
         } else if (dp.isTablet) {
             int alignedOptionIndex = 0;
             if (getRecentsView().isOnGridBottomRow(menuContainer.getTaskView()) && dp.isLandscape) {
@@ -1660,7 +1659,7 @@
         return (RecentsView) getParent();
     }
 
-    PagedOrientationHandler getPagedOrientationHandler() {
+    RecentsPagedOrientationHandler getPagedOrientationHandler() {
         return getRecentsView().mOrientationState.getOrientationHandler();
     }
 
@@ -1721,12 +1720,11 @@
         int expectedWidth;
         int expectedHeight;
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+        final int thumbnailPadding = deviceProfile.overviewTaskThumbnailTopMarginPx;
+        final Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize();
+        final int taskWidth = lastComputedTaskSize.width();
+        final int taskHeight = lastComputedTaskSize.height();
         if (deviceProfile.isTablet) {
-            final int thumbnailPadding = deviceProfile.overviewTaskThumbnailTopMarginPx;
-            final Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize();
-            final int taskWidth = lastComputedTaskSize.width();
-            final int taskHeight = lastComputedTaskSize.height();
-
             int boxWidth;
             int boxHeight;
             boolean isFocusedTask = isFocusedTask();
@@ -1759,8 +1757,10 @@
         } else {
             nonGridScale = 1f;
             boxTranslationY = 0f;
-            expectedWidth = ViewGroup.LayoutParams.MATCH_PARENT;
-            expectedHeight = ViewGroup.LayoutParams.MATCH_PARENT;
+            expectedWidth = enableOverviewIconMenu() ? taskWidth : LayoutParams.MATCH_PARENT;
+            expectedHeight = enableOverviewIconMenu()
+                    ? taskHeight + thumbnailPadding
+                    : LayoutParams.MATCH_PARENT;
         }
 
         setNonGridScale(nonGridScale);
diff --git a/quickstep/src/com/android/quickstep/views/TaskViewIcon.java b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java
index 4e82725..94739cb 100644
--- a/quickstep/src/com/android/quickstep/views/TaskViewIcon.java
+++ b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java
@@ -43,6 +43,11 @@
     void setContentAlpha(float alpha);
 
     /**
+     * Sets the opacity of the view for modal state.
+     */
+    void setModalAlpha(float alpha);
+
+    /**
      * Returns this icon view's drawable.
      */
     @Nullable Drawable getDrawable();
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index b12d98b..37dde10 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -26,6 +26,8 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
@@ -36,11 +38,14 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.text.TextUtils;
 
+import androidx.test.core.content.pm.ApplicationInfoBuilder;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
 import com.android.launcher3.util.LauncherLayoutBuilder;
@@ -50,6 +55,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -61,6 +67,9 @@
 @RunWith(AndroidJUnit4.class)
 public final class WidgetsPredicationUpdateTaskTest {
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private AppWidgetProviderInfo mApp1Provider1;
     private AppWidgetProviderInfo mApp1Provider2;
     private AppWidgetProviderInfo mApp2Provider1;
@@ -75,6 +84,7 @@
 
     @Before
     public void setup() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS);
         mModelHelper = new LauncherModelHelper();
 
         mUserHandle = myUserHandle();
@@ -93,6 +103,12 @@
         allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1,
                 mApp4Provider1, mApp4Provider2, mApp5Provider1);
 
+        doAnswer(i -> {
+            String pkg = i.getArgument(0);
+            return ApplicationInfoBuilder.newBuilder().setPackageName(pkg).setName(
+                    "App " + pkg).build();
+        }).when(mModelHelper.sandboxContext.getPackageManager())
+                .getApplicationInfo(anyString(), anyInt());
         AppWidgetManager manager = mModelHelper.sandboxContext.spyService(AppWidgetManager.class);
         doReturn(allWidgets).when(manager).getInstalledProviders();
         doReturn(allWidgets).when(manager).getInstalledProvidersForProfile(eq(myUserHandle()));
@@ -140,12 +156,16 @@
             // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
             //    excluded from the result.
             // 2. app3 doesn't have a widget.
-            // 3. only 1 widget is picked from app1 because we only want to promote one widget per app.
+            // 3. only 1 widget is picked from app1 because we only want to promote one widget
+            // per app.
             List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
                     .stream()
                     .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
                     .collect(Collectors.toList());
             assertThat(recommendedWidgets).hasSize(2);
+            recommendedWidgets.forEach(pendingAddWidgetInfo ->
+                    assertThat(pendingAddWidgetInfo.recommendationCategory).isNotNull()
+            );
             assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
             assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
         });
@@ -179,6 +199,9 @@
                     .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
                     .collect(Collectors.toList());
             assertThat(recommendedWidgets).hasSize(2);
+            recommendedWidgets.forEach(pendingAddWidgetInfo ->
+                    assertThat(pendingAddWidgetInfo.recommendationCategory).isNotNull()
+            );
             // Another widget from the same package
             assertWidgetInfo(recommendedWidgets.get(0).info, mApp4Provider2);
             assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
@@ -192,7 +215,7 @@
     }
 
     private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List<AppTarget> appTargets) {
-       return new WidgetsPredictionUpdateTask(
+        return new WidgetsPredictionUpdateTask(
                 new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"),
                 appTargets);
     }
diff --git a/quickstep/tests/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchControllerTest.kt b/quickstep/tests/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchControllerTest.kt
new file mode 100644
index 0000000..119b862
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchControllerTest.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.uioverrides.touchcontrollers
+
+import android.view.MotionEvent
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Launcher
+import com.android.launcher3.ui.AbstractLauncherUiTest
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class StatusBarTouchControllerTest : AbstractLauncherUiTest() {
+    @Before
+    @Throws(Exception::class)
+    fun setup() {
+        super.setUp()
+        initialize(this)
+    }
+
+    @Test
+    fun interceptActionDown_canIntercept() {
+        executeOnLauncher { launcher ->
+            val underTest = StatusBarTouchController(launcher)
+            assertFalse(underTest.mCanIntercept)
+            val downEvent = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+            underTest.onControllerInterceptTouchEvent(downEvent)
+
+            assertTrue(underTest.mCanIntercept)
+        }
+    }
+
+    @Test
+    fun interceptVerticalActionMove_handledAndSetSlippery() {
+        executeOnLauncher { launcher ->
+            val underTest = StatusBarTouchController(launcher)
+            val downEvent = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+            underTest.onControllerInterceptTouchEvent(downEvent)
+            val w = launcher.window
+            assertEquals(0, w.attributes.flags and WindowManager.LayoutParams.FLAG_SLIPPERY)
+            val moveEvent =
+                MotionEvent.obtain(
+                    2,
+                    2,
+                    MotionEvent.ACTION_MOVE,
+                    underTest.mTouchSlop,
+                    underTest.mTouchSlop + 10,
+                    0
+                )
+
+            val handled = underTest.onControllerInterceptTouchEvent(moveEvent)
+
+            assertTrue(handled)
+            assertEquals(
+                WindowManager.LayoutParams.FLAG_SLIPPERY,
+                w.attributes.flags and WindowManager.LayoutParams.FLAG_SLIPPERY
+            )
+        }
+    }
+
+    @Test
+    fun interceptHorizontalActionMove_not_handled() {
+        executeOnLauncher { launcher ->
+            val underTest = StatusBarTouchController(launcher)
+            val downEvent = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+            underTest.onControllerInterceptTouchEvent(downEvent)
+            val moveEvent =
+                MotionEvent.obtain(
+                    2,
+                    2,
+                    MotionEvent.ACTION_MOVE,
+                    underTest.mTouchSlop + 10,
+                    underTest.mTouchSlop,
+                    0
+                )
+
+            val handled = underTest.onControllerInterceptTouchEvent(moveEvent)
+
+            assertFalse(handled)
+        }
+    }
+
+    @Test
+    fun interceptActionMoveAsFirstGestureEvent_notCrashedNorHandled() {
+        executeOnLauncher { launcher ->
+            val underTest = StatusBarTouchController(launcher)
+            underTest.mCanIntercept = true
+            val moveEvent = MotionEvent.obtain(2, 2, MotionEvent.ACTION_MOVE, 10f, 10f, 0)
+
+            val handled = underTest.onControllerInterceptTouchEvent(moveEvent)
+
+            assertFalse(handled)
+        }
+    }
+
+    @Test
+    fun handleActionUp_setNotSlippery() {
+        executeOnLauncher { launcher: Launcher ->
+            val underTest = StatusBarTouchController(launcher)
+            underTest.mCanIntercept = true
+            underTest.setWindowSlippery(true)
+            val moveEvent = MotionEvent.obtain(2, 2, MotionEvent.ACTION_UP, 10f, 10f, 0)
+
+            val handled = underTest.onControllerTouchEvent(moveEvent)
+
+            assertTrue(handled)
+            assertEquals(
+                0,
+                launcher.window.attributes.flags and WindowManager.LayoutParams.FLAG_SLIPPERY
+            )
+        }
+    }
+
+    @Test
+    fun handleActionCancel_setNotSlippery() {
+        executeOnLauncher { launcher ->
+            val underTest = StatusBarTouchController(launcher)
+            underTest.mCanIntercept = true
+            underTest.setWindowSlippery(true)
+            val moveEvent = MotionEvent.obtain(2, 2, MotionEvent.ACTION_CANCEL, 10f, 10f, 0)
+
+            val handled = underTest.onControllerTouchEvent(moveEvent)
+
+            assertTrue(handled)
+            assertEquals(
+                0,
+                launcher.window.attributes.flags and WindowManager.LayoutParams.FLAG_SLIPPERY
+            )
+        }
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
index 3f806d1..7c1b7f3 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.ui.AbstractLauncherUiTest.initialize;
 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
 import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
@@ -25,6 +24,7 @@
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
 
+import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask;
 import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.rule.TestStabilityRule;
@@ -86,7 +86,8 @@
         taskMenu.touchOutsideTaskMenuToDismiss();
 
         OverviewTaskMenu splitMenu =
-                mLauncher.goHome().switchToOverview().getCurrentTask().tapSplitTaskMenu();
+                mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu(
+                        OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT);
         assertTrue("App info item not appearing in expanded split task's menu.",
                 splitMenu.hasMenuItem("App info"));
         splitMenu.touchOutsideTaskMenuToDismiss();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 7191f70..a050464 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -22,6 +22,9 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.tapl.KeyboardQuickSwitch;
+import com.android.launcher3.tapl.LaunchedAppState;
+import com.android.launcher3.tapl.Taskbar;
+import com.android.launcher3.taskbar.KeyboardQuickSwitchController;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 
 import org.junit.Assume;
@@ -35,6 +38,7 @@
     private enum TestSurface {
         HOME(true),
         LAUNCHED_APP(false),
+        TASKBAR_ALL_APPS(false),
         HOME_ALL_APPS(true),
         WIDGETS(true);
 
@@ -49,7 +53,7 @@
         DISMISS(0),
         LAUNCH_LAST_APP(0),
         LAUNCH_SELECTED_APP(1),
-        LAUNCH_OVERVIEW(5);
+        LAUNCH_OVERVIEW(KeyboardQuickSwitchController.MAX_TASKS - 1);
 
         private final int mNumAdditionalRunningTasks;
 
@@ -81,6 +85,11 @@
     }
 
     @Test
+    public void testDismiss_fromTaskbarAllApps() {
+        runTest(TestSurface.TASKBAR_ALL_APPS, TestCase.DISMISS);
+    }
+
+    @Test
     public void testDismiss_fromHomeAllApps() {
         runTest(TestSurface.HOME_ALL_APPS, TestCase.DISMISS);
     }
@@ -101,6 +110,11 @@
     }
 
     @Test
+    public void testLaunchLastTask_fromTaskbarAllApps() {
+        runTest(TestSurface.TASKBAR_ALL_APPS, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
     public void testLaunchLastTask_fromHomeAllApps() {
         runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_LAST_APP);
     }
@@ -121,6 +135,11 @@
     }
 
     @Test
+    public void testLaunchSelectedTask_fromTaskbarAllApps() {
+        runTest(TestSurface.TASKBAR_ALL_APPS, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
     public void testLaunchSelectedTask_fromHomeAllApps() {
         runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_SELECTED_APP);
     }
@@ -141,6 +160,11 @@
     }
 
     @Test
+    public void testLaunchOverviewTask_fromTaskbarAllApps() {
+        runTest(TestSurface.TASKBAR_ALL_APPS, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    @Test
     public void testLaunchOverviewTask_fromHomeAllApps() {
         runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_OVERVIEW);
     }
@@ -164,6 +188,12 @@
                 mLauncher.setIgnoreTaskbarVisibility(true);
                 kqs = mLauncher.getLaunchedAppState().showQuickSwitchView();
                 break;
+            case TASKBAR_ALL_APPS:
+                LaunchedAppState launchedApp = mLauncher.getLaunchedAppState();
+                Taskbar taskbar = mLauncher.isTransientTaskbar()
+                        ? launchedApp.swipeUpToUnstashTaskbar() : launchedApp.getTaskbar();
+                kqs = taskbar.openAllApps().showQuickSwitchView();
+                break;
             case HOME_ALL_APPS:
                 kqs = mLauncher.goHome().switchToAllApps().showQuickSwitchView();
                 break;
@@ -196,7 +226,9 @@
                 if (!testSurface.mInitialFocusAtZero) {
                     kqs.moveFocusBackward();
                 }
-                kqs.launchFocusedOverviewTask();
+                kqs.launchFocusedOverviewTask()
+                        // Check that the correct task was focused
+                        .launchFocusedTaskByEnterKey(CALCULATOR_APP_PACKAGE);
                 break;
             default:
                 throw new IllegalStateException("Cannot run test case: " + testCase);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index fcb8320..360d1a7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -35,9 +35,9 @@
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.Until;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.tapl.LaunchedAppState;
 import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.Overview;
@@ -394,7 +394,7 @@
                 READ_DEVICE_CONFIG_PERMISSION);
         // Debug if we need to goHome to prevent wrong previous state b/315525621
         mLauncher.goHome();
-        assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
+        assumeFalse(Flags.enablePredictiveBackGesture());
         mLauncher.getWorkspace().switchToAllApps().pressBackToWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index 3465f23..b0e91e4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -28,7 +28,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.Flags;
 import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
@@ -77,7 +77,7 @@
     @NavigationModeSwitch(mode = ZERO_BUTTON)
     public void pressBack() throws Exception {
         assumeTrue(mLauncher.isTablet());
-        assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
+        assumeFalse(Flags.enablePredictiveBackGesture());
         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
 
         try {
diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
index b365173..9fa4b79 100644
--- a/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
+++ b/quickstep/tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -163,8 +163,7 @@
                 helper.sandboxContext.allow(SystemUiProxy.INSTANCE);
                 int rotation = mDisplaySize.x > mDisplaySize.y
                         ? Surface.ROTATION_90 : Surface.ROTATION_0;
-                CachedDisplayInfo cdi =
-                        new CachedDisplayInfo(mDisplaySize, rotation, new Rect());
+                CachedDisplayInfo cdi = new CachedDisplayInfo(mDisplaySize, rotation);
                 WindowBounds wm = new WindowBounds(
                         new Rect(0, 0, mDisplaySize.x, mDisplaySize.y),
                         mDisplayInsets);
@@ -186,7 +185,7 @@
 
                 ArrayMap<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache =
                         new ArrayMap<>();
-                perDisplayBoundsCache.put(cdi.normalize(), allBounds);
+                perDisplayBoundsCache.put(cdi.normalize(wmProxy), allBounds);
 
                 Configuration configuration = new Configuration();
                 configuration.densityDpi = mDensityDpi;
diff --git a/res/drawable/ic_encrypted_with_background.xml b/res/drawable/ic_private_space_with_background.xml
similarity index 65%
rename from res/drawable/ic_encrypted_with_background.xml
rename to res/drawable/ic_private_space_with_background.xml
index c439b55..59a33dd 100644
--- a/res/drawable/ic_encrypted_with_background.xml
+++ b/res/drawable/ic_private_space_with_background.xml
@@ -21,6 +21,9 @@
         android:pathData="M48 24A24 24 0 0 1 0 24A24 24 0 0 1 48 24Z"
         android:fillColor="?attr/materialColorOutlineVariant" />
     <path
-        android:pathData="M24.0002 10.667L13.3335 14.667V22.787C13.3335 29.5203 17.8802 35.8003 24.0002 37.3337C30.1202 35.8003 34.6668 29.5203 34.6668 22.787V14.667L24.0002 10.667ZM32.0002 22.787C32.0002 28.1203 28.6002 33.0537 24.0002 34.5603C19.4002 33.0537 16.0002 28.1337 16.0002 22.787V16.5203L24.0002 13.5203L32.0002 16.5203V22.787ZM25.2402 23.6937L26.0002 28.0003H22.0002L22.7602 23.6937C21.9068 23.2537 21.3335 22.3603 21.3335 21.3337C21.3335 19.867 22.5335 18.667 24.0002 18.667C25.4668 18.667 26.6668 19.867 26.6668 21.3337C26.6668 22.3603 26.0935 23.2537 25.2402 23.6937Z"
+        android:pathData="M33.3333 14.6667V33.3333H14.6667V14.6667H33.3333ZM33.3333 12H14.6667C13.2 12 12 13.2 12 14.6667V33.3333C12 34.8 13.2 36 14.6667 36H33.3333C34.8 36 36 34.8 36 33.3333V14.6667C36 13.2 34.8 12 33.3333 12Z"
+        android:fillColor="?attr/materialColorOnSurface" />
+    <path
+        android:pathData="M25.2397 24.3597L25.9997 28.6663H21.9997L22.7597 24.3597C21.9063 23.9197 21.333 23.0263 21.333 21.9997C21.333 20.533 22.533 19.333 23.9997 19.333C25.4663 19.333 26.6663 20.533 26.6663 21.9997C26.6663 23.0263 26.093 23.9197 25.2397 24.3597Z"
         android:fillColor="?attr/materialColorOnSurface" />
 </vector>
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
index e718d9c..bddfcfc 100644
--- a/res/values-sw600dp/config.xml
+++ b/res/values-sw600dp/config.xml
@@ -18,6 +18,9 @@
     <!-- The duration of the PagedView page snap animation -->
     <integer name="config_pageSnapAnimationDuration">550</integer>
 
+    <!-- The duration of the PagedView page snap animation -->
+    <integer name="config_keyboardTaskFocusSnapAnimationDuration">400</integer>
+
     <!-- The duration of the Widget picker opening and closing animation -->
     <integer name="config_bottomSheetOpenDuration">500</integer>
     <integer name="config_bottomSheetCloseDuration">500</integer>
diff --git a/res/values/config.xml b/res/values/config.xml
index 29c4e66..1b74238 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -43,6 +43,9 @@
     <!-- The duration of the PagedView page snap animation -->
     <integer name="config_pageSnapAnimationDuration">750</integer>
 
+    <!-- The duration of the PagedView page snap animation -->
+    <integer name="config_keyboardTaskFocusSnapAnimationDuration">750</integer>
+
     <!-- View tag key used to store SpringAnimation data. -->
     <item type="id" name="spring_animation_tag" />
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3016559..3aa4a77 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -56,7 +56,7 @@
     <!-- App Widget resize frame -->
     <dimen name="widget_handle_margin">13dp</dimen>
     <dimen name="resize_frame_background_padding">24dp</dimen>
-    <dimen name="resize_frame_margin">22dp</dimen>
+    <dimen name="resize_frame_margin">23dp</dimen>
     <dimen name="resize_frame_invalid_drag_across_two_panel_opacity_margin">24dp</dimen>
 
     <!-- App widget reconfigure button -->
@@ -196,6 +196,8 @@
     <dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
     <dimen name="widget_list_entry_spacing">2dp</dimen>
     <dimen name="widget_list_horizontal_margin">16dp</dimen>
+    <!-- Margin applied to the recycler view with search bar & the list of widget apps below it. -->
+    <dimen name="widget_list_left_pane_horizontal_margin">0dp</dimen>
     <dimen name="widget_list_horizontal_margin_two_pane">24dp</dimen>
 
     <dimen name="widget_preview_shadow_blur">0.5dp</dimen>
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 1b2cebd..60a6be6 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -34,6 +34,7 @@
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
 import android.os.UserHandle;
 import android.util.Log;
 
@@ -108,9 +109,11 @@
         launcherApps.registerCallback(callbacks);
         mOnTerminateCallback.add(() ->
                 mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
-        if (Flags.enableSupportForArchiving()) {
-            launcherApps.setArchiveCompatibilityOptions(/* enableIconOverlay= */ true,
-                    /* enableUnarchivalConfirmation= */ false);
+
+        if (Utilities.enableSupportForArchiving()) {
+            ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
+            params.setEnableUnarchivalConfirmation(false);
+            launcherApps.setArchiveCompatibility(params);
         }
 
         SimpleBroadcastReceiver modelChangeReceiver =
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index f355ae7..ca83245 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -118,7 +118,8 @@
     private float mTotalMotion;
     // Used in special cases where the fling checks can be relaxed for an intentional gesture
     private boolean mAllowEasyFling;
-    protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+    private PagedOrientationHandler mOrientationHandler =
+            PagedOrientationHandler.DEFAULT;
 
     private final ArrayList<Runnable> mOnPageScrollsInitializedCallbacks = new ArrayList<>();
 
@@ -231,6 +232,14 @@
         return getChildAt(index);
     }
 
+    protected PagedOrientationHandler getPagedOrientationHandler() {
+        return mOrientationHandler;
+    }
+
+    protected void setOrientationHandler(PagedOrientationHandler orientationHandler) {
+        this.mOrientationHandler = orientationHandler;
+    }
+
     /**
      * Updates the scroll of the current page immediately to its final scroll position.  We use this
      * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of
@@ -628,6 +637,11 @@
         mMinFlingVelocity = res.getDimensionPixelSize(R.dimen.min_fling_velocity);
         mMinSnapVelocity = res.getDimensionPixelSize(R.dimen.min_page_snap_velocity);
         mPageSnapAnimationDuration = res.getInteger(R.integer.config_pageSnapAnimationDuration);
+        onVelocityValuesUpdated();
+    }
+
+    protected void onVelocityValuesUpdated() {
+        // Overridden in RecentsView
     }
 
     @Override
@@ -1573,7 +1587,7 @@
     @Override
     public void requestChildFocus(View child, View focused) {
         super.requestChildFocus(child, focused);
-        if (!shouldHandleRequestChildFocus()) {
+        if (!shouldHandleRequestChildFocus(child)) {
             return;
         }
         // In case the device is controlled by a controller, mCurrentPage isn't updated properly
@@ -1589,7 +1603,7 @@
         }
     }
 
-    protected boolean shouldHandleRequestChildFocus() {
+    protected boolean shouldHandleRequestChildFocus(View child) {
         return true;
     }
 
@@ -1643,7 +1657,7 @@
     }
 
     protected void snapToDestination() {
-        snapToPage(getDestinationPage(), mPageSnapAnimationDuration);
+        snapToPage(getDestinationPage(), getSnapAnimationDuration());
     }
 
     // We want the duration of the page snap animation to be influenced by the distance that
@@ -1667,7 +1681,7 @@
         if (Math.abs(velocity) < mMinFlingVelocity) {
             // If the velocity is low enough, then treat this more as an automatic page advance
             // as opposed to an apparent physical response to flinging
-            return snapToPage(whichPage, mPageSnapAnimationDuration);
+            return snapToPage(whichPage, getSnapAnimationDuration());
         }
 
         // Here we compute a "distance" that will be used in the computation of the overall
@@ -1689,12 +1703,16 @@
         return snapToPage(whichPage, delta, duration);
     }
 
+    protected int getSnapAnimationDuration() {
+        return mPageSnapAnimationDuration;
+    }
+
     public boolean snapToPage(int whichPage) {
-        return snapToPage(whichPage, mPageSnapAnimationDuration);
+        return snapToPage(whichPage, getSnapAnimationDuration());
     }
 
     public boolean snapToPageImmediately(int whichPage) {
-        return snapToPage(whichPage, mPageSnapAnimationDuration, true);
+        return snapToPage(whichPage, getSnapAnimationDuration(), true);
     }
 
     public boolean snapToPage(int whichPage, int duration) {
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 2b886e4..d44438f 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -830,4 +830,10 @@
                 // No-Op
         }
     }
+
+    /** Encapsulates two flag checks into a single one. */
+    public static boolean enableSupportForArchiving() {
+        return Flags.enableSupportForArchiving()
+                || getSystemProperty("pm.archiving.enabled", "false").equals("true");
+    }
 }
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index e65e614..a846e68 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -415,6 +415,8 @@
                         screenId, coordinates[0], coordinates[1]);
                 bindItem(info, accessibility);
             } else if (item instanceof FolderInfo fi) {
+                Workspace<?> workspace = mContext.getWorkspace();
+                workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
                 mContext.getModelWriter().addItemToDatabase(fi,
                         LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0],
                         coordinates[1]);
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 930196d..ae2849e 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -1156,13 +1156,15 @@
 
         applyAdapterSideAndBottomPaddings(grid);
 
+        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
         // Ignore left/right insets on tablet because we are already centered in-screen.
-        if (grid.isPhone) {
-            MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
+        if (grid.isTablet) {
+            mlp.leftMargin = mlp.rightMargin = 0;
+        } else {
             mlp.leftMargin = insets.left;
             mlp.rightMargin = insets.right;
-            setLayoutParams(mlp);
         }
+        setLayoutParams(mlp);
 
         if (!grid.isVerticalBarLayout() || FeatureFlags.enableResponsiveWorkspace()) {
             int topPadding = grid.allAppsPadding.top;
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 557ec48..6422943 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
@@ -109,7 +110,7 @@
         itemInfo.bitmap = bitmapInfo;
         itemInfo.contentDescription = context.getResources().getString(
                 com.android.launcher3.R.string.ps_add_button_content_description);
-        itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP;
+        itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP | FLAG_NOT_PINNABLE;
 
         BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON);
         item.itemInfo = itemInfo;
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
index bc55597..fcdfaa6 100644
--- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -55,6 +55,9 @@
         assert quietModeButton != null;
         addQuietModeButton(quietModeButton);
 
+        //Trigger lock/unlock action from header.
+        addHeaderOnClickListener(parent);
+
         //Add image and action for private space settings button
         ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button);
         assert settingsButton != null;
@@ -71,26 +74,35 @@
             case STATE_ENABLED -> {
                 quietModeButton.setVisibility(View.VISIBLE);
                 quietModeButton.setImageResource(R.drawable.bg_ps_lock_button);
-                quietModeButton.setOnClickListener(
-                        view -> {
-                            mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP);
-                            mPrivateProfileManager.lockPrivateProfile();
-                        });
+                quietModeButton.setOnClickListener(view -> lockAction());
             }
             case STATE_DISABLED -> {
                 quietModeButton.setVisibility(View.VISIBLE);
                 quietModeButton.setImageResource(R.drawable.bg_ps_unlock_button);
-                quietModeButton.setOnClickListener(
-                        view -> {
-                            mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP);
-                            mPrivateProfileManager.unlockPrivateProfile((this::
-                                    onPrivateProfileUnlocked));
-                        });
+                quietModeButton.setOnClickListener(view -> unLockAction());
             }
             default -> quietModeButton.setVisibility(View.GONE);
         }
     }
 
+    private void addHeaderOnClickListener(RelativeLayout header) {
+        if (mPrivateProfileManager.getCurrentState() == STATE_DISABLED) {
+            header.setOnClickListener(view -> unLockAction());
+        } else {
+            header.setOnClickListener(null);
+        }
+    }
+
+    private void unLockAction() {
+        mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP);
+        mPrivateProfileManager.unlockPrivateProfile((this::onPrivateProfileUnlocked));
+    }
+
+    private void lockAction() {
+        mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP);
+        mPrivateProfileManager.lockPrivateProfile();
+    }
+
     private void addPrivateSpaceSettingsButton(ImageButton settingsButton) {
         if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED
                 && mPrivateProfileManager.isPrivateSpaceSettingsAvailable()) {
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 4427a49..f9d047b 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -20,6 +20,7 @@
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.text.style.SuggestionSpan;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnFocusChangeListener;
@@ -42,6 +43,7 @@
         implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener,
         OnFocusChangeListener {
 
+    private static final String TAG = "AllAppsSearchBarController";
     protected ActivityContext mLauncher;
     protected SearchCallback<AdapterItem> mCallback;
     protected ExtendedEditText mInput;
@@ -122,6 +124,7 @@
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 
         if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) {
+            Log.i(TAG, "User tapped ime search button");
             // selectFocusedView should return SearchTargetEvent that is passed onto onClick
             return mLauncher.getAppsView().getMainAdapterProvider().launchHighlightedItem();
         }
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
index 1d73441..9b85a65 100644
--- a/src/com/android/launcher3/apppairs/AppPairIcon.java
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -45,6 +46,8 @@
  * member apps are set into these rectangles.
  */
 public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
+    private static final String TAG = "AppPairIcon";
+
     // A view that holds the app pair icon graphic.
     private AppPairIconGraphic mIconGraphic;
     // A view that holds the app pair's title.
@@ -96,8 +99,7 @@
         icon.mAppPairName.setText(appPairInfo.title);
 
         // Set up accessibility
-        icon.setContentDescription(icon.getAccessibilityTitle(
-                appPairInfo.contents.get(0).title, appPairInfo.contents.get(1).title));
+        icon.setContentDescription(icon.getAccessibilityTitle(appPairInfo));
         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
 
         return icon;
@@ -106,7 +108,14 @@
     /**
      * Returns a formatted accessibility title for app pairs.
      */
-    public String getAccessibilityTitle(CharSequence app1, CharSequence app2) {
+    public String getAccessibilityTitle(FolderInfo appPairInfo) {
+        if (appPairInfo.contents.size() != 2) {
+            Log.wtf(TAG, "AppPair contents not 2, size: " + appPairInfo.contents.size());
+            return "";
+        }
+
+        CharSequence app1 = appPairInfo.contents.get(0).title;
+        CharSequence app2 = appPairInfo.contents.get(1).title;
         return getContext().getString(R.string.app_pair_name_format, app1, app2);
     }
 
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
index ce354b6..65c270a 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -93,7 +93,7 @@
     private fun applyIcons(contents: ArrayList<WorkspaceItemInfo>) {
         // App pair should always contain 2 members; if not 2, return to avoid a crash loop
         if (contents.size != 2) {
-            Log.w(TAG, "AppPair contents not 2, size: " + contents.size, Throwable())
+            Log.wtf(TAG, "AppPair contents not 2, size: " + contents.size, Throwable())
             return
         }
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 40c368b..e2902e9 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -199,11 +199,6 @@
             "ENABLE_SMARTSPACE_REMOVAL", DISABLED, "Enable SmartSpace removal for "
             + "home screen");
 
-    // TODO(Block 10): Clean up flags
-    public static final BooleanFlag ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION = getDebugFlag(270614790,
-            "ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION", DISABLED,
-            "Enables predictive back animation from all apps and widgets to home");
-
     // TODO(Block 11): Clean up flags
     public static final BooleanFlag FOLDABLE_SINGLE_PAGE = getDebugFlag(270395274,
             "FOLDABLE_SINGLE_PAGE", DISABLED, "Use a single page for the workspace");
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 3ccde0a..b6e5977 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.dragndrop;
 
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
+
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -29,8 +31,10 @@
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
+import com.android.launcher3.Flags;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.ActivityContext;
@@ -221,6 +225,12 @@
         }
     }
 
+    protected boolean isItemPinnable() {
+        return !Flags.privateSpaceRestrictItemDrag()
+                || !(mDragObject.dragInfo instanceof ItemInfoWithIcon itemInfoWithIcon)
+                || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0;
+    }
+
     public Optional<InstanceId> getLogInstanceId() {
         return Optional.ofNullable(mDragObject)
                 .map(dragObject -> dragObject.logInstanceId);
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
index da6f446..f3708a2 100644
--- a/src/com/android/launcher3/dragndrop/LauncherDragController.java
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -149,9 +149,10 @@
 
         handleMoveEvent(mLastTouch.x, mLastTouch.y);
 
-        if (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null) {
+        if (!isItemPinnable()
+                || (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null)) {
             // If it is an internal drag and the touch is already complete, cancel immediately
-            MAIN_EXECUTOR.submit(this::cancelDrag);
+            MAIN_EXECUTOR.post(this::cancelDrag);
         }
         return dragView;
     }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7ae70e0..2f3f029 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -800,6 +800,14 @@
             return;
         }
 
+        int size = getIconsInReadingOrder().size();
+        if (size <= 1) {
+            Log.d(TAG, "Couldn't animate folder closed because there's " + size + " icons");
+            closeComplete(false);
+            post(this::announceAccessibilityChanges);
+            return;
+        }
+
         mContent.completePendingPageChanges();
         mContent.snapToPageImmediately(mContent.getDestinationPage());
 
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 5e86bd6..96a8da9 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemFactory;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -102,6 +103,11 @@
                             Objects.requireNonNull(item.getIntent()))) {
                         continue;
                     }
+
+                    if (item instanceof ItemInfoWithIcon
+                            && ((ItemInfoWithIcon) item).isArchived()) {
+                        continue;
+                    }
                 }
 
                 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index b41f011..8659471 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.model;
 
-import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
 import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
@@ -34,6 +33,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.AppFilter;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -330,7 +330,7 @@
                             PackageManagerHelper.getLoadingProgress(info),
                             PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
                     applicationInfo.intent = launchIntent;
-                    if (enableSupportForArchiving()) {
+                    if (Utilities.enableSupportForArchiving()) {
                         // In case an app is archived, the respective item flag corresponding to
                         // archiving should also be applied during package updates
                         if (info.getActivityInfo().isArchived) {
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index 9a3abd4..59f453a 100644
--- a/src/com/android/launcher3/model/ItemInstallQueue.java
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -18,10 +18,12 @@
 
 import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.appwidget.AppWidgetManager;
@@ -276,6 +278,7 @@
             return intent;
         }
 
+        @SuppressWarnings("NewApi")
         public Pair<ItemInfo, Object> getItemInfo(Context context) {
             switch (itemType) {
                 case ITEM_TYPE_APPLICATION: {
@@ -297,6 +300,9 @@
                     } else {
                         lai = laiList.get(0);
                         si.intent = makeLaunchIntent(lai);
+                        if (enableSupportForArchiving() && lai.getActivityInfo().isArchived) {
+                            si.runtimeStatusFlags |= FLAG_ARCHIVED;
+                        }
                     }
                     LauncherAppState.getInstance(context).getIconCache()
                             .getTitleAndIcon(si, () -> lai, usePackageIcon, false);
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 736b80a..71ab51c 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -18,7 +18,6 @@
 
 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
 import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
-import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
@@ -421,7 +420,7 @@
 
             final HashMap<PackageUserKey, SessionInfo> installingPkgs =
                     mSessionHelper.getActiveSessions();
-            if (enableSupportForArchiving()) {
+            if (Utilities.enableSupportForArchiving()) {
                 mInstallingPkgsCached = installingPkgs;
             }
             installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
@@ -656,7 +655,7 @@
             for (int i = 0; i < apps.size(); i++) {
                 LauncherActivityInfo app = apps.get(i);
                 AppInfo appInfo = new AppInfo(app, mUserCache.getUserInfo(user), quietMode);
-                if (enableSupportForArchiving() && app.getApplicationInfo().isArchived) {
+                if (Utilities.enableSupportForArchiving() && app.getApplicationInfo().isArchived) {
                     // For archived apps, include progress info in case there is a pending
                     // install session post restart of device.
                     String appPackageName = app.getApplicationInfo().packageName;
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 529a8f9..a41b663 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.model;
 
-import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
@@ -39,6 +38,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.FileLog;
@@ -274,7 +274,7 @@
                                     PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
                             // In case an app is archived, we need to make sure that archived state
                             // in WorkspaceItemInfo is refreshed.
-                            if (enableSupportForArchiving() && !activities.isEmpty()) {
+                            if (Utilities.enableSupportForArchiving() && !activities.isEmpty()) {
                                 boolean newArchivalState = activities.get(
                                         0).getActivityInfo().isArchived;
                                 if (newArchivalState != si.isArchived()) {
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 44c41c1..31ae7c2 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -26,7 +26,6 @@
 import android.text.TextUtils
 import android.util.Log
 import android.util.LongSparseArray
-import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
@@ -323,7 +322,7 @@
             }
             if (
                 (c.restoreFlag != 0 ||
-                    Flags.enableSupportForArchiving() &&
+                    Utilities.enableSupportForArchiving() &&
                         activityInfo != null &&
                         activityInfo.applicationInfo.isArchived) && !TextUtils.isEmpty(targetPkg)
             ) {
@@ -338,7 +337,7 @@
                         null // For archived apps, include progress info in case there is
                     // a pending install session post restart of device.
                     ||
-                        (Flags.enableSupportForArchiving() &&
+                        (Utilities.enableSupportForArchiving() &&
                             activityInfo.applicationInfo.isArchived)
                 ) {
                     val installProgress = (si.getProgress() * 100).toInt()
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index 72eda6c..b213fe3 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.model.data;
 
-import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 
 import android.content.ComponentName;
@@ -179,7 +178,7 @@
         if (PackageManagerHelper.isAppSuspended(appInfo)) {
             info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
         }
-        if (enableSupportForArchiving() && lai.getActivityInfo().isArchived) {
+        if (Utilities.enableSupportForArchiving() && lai.getActivityInfo().isArchived) {
             info.runtimeStatusFlags |= FLAG_ARCHIVED;
         }
         info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index c8ab09c..e46c502 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -16,14 +16,13 @@
 
 package com.android.launcher3.model.data;
 
-import static com.android.launcher3.Flags.enableSupportForArchiving;
-
 import android.content.Context;
 import android.content.Intent;
 import android.os.Process;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
 import com.android.launcher3.icons.FastBitmapDrawable;
@@ -158,7 +157,7 @@
     /**
      * Returns true if the app corresponding to the item is archived. */
     public boolean isArchived() {
-        if (!enableSupportForArchiving()) {
+        if (!Utilities.enableSupportForArchiving()) {
             return false;
         }
         return (runtimeStatusFlags & FLAG_ARCHIVED) != 0;
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index fd1b64f..df369c6 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -365,6 +365,13 @@
     @Override
     public void setMarkersCount(int numMarkers) {
         mNumPages = numMarkers;
+
+        // If the last page gets removed we want to go to the previous page.
+        if (mNumPages == mActivePage) {
+            mActivePage--;
+            CURRENT_POSITION.set(this, (float) mActivePage);
+        }
+
         requestLayout();
     }
 
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 0d47462..605ef16 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.pm;
 
-import static com.android.launcher3.Flags.enableSupportForArchiving;
-
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -33,6 +31,7 @@
 
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.SessionCommitReceiver;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.ItemInstallQueue;
 import com.android.launcher3.util.IntArray;
@@ -228,7 +227,8 @@
 
     public boolean verifySessionInfo(@Nullable final PackageInstaller.SessionInfo sessionInfo) {
         // For archived apps we always want to show promise icons and the checks below don't apply.
-        if (enableSupportForArchiving() && sessionInfo != null && sessionInfo.isUnarchival()) {
+        if (Utilities.enableSupportForArchiving() && sessionInfo != null
+                && sessionInfo.isUnarchival()) {
             return true;
         }
 
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index e4a2045..eacbc11 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.pm;
 
-import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle;
 import static com.android.launcher3.pm.PackageInstallInfo.STATUS_FAILED;
 import static com.android.launcher3.pm.PackageInstallInfo.STATUS_INSTALLED;
@@ -32,6 +31,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.lang.ref.WeakReference;
@@ -80,7 +80,8 @@
 
         helper.tryQueuePromiseAppIcon(sessionInfo);
 
-        if (enableSupportForArchiving() && sessionInfo != null && sessionInfo.isUnarchival()) {
+        if (Utilities.enableSupportForArchiving() && sessionInfo != null
+                && sessionInfo.isUnarchival()) {
             // For archived apps, icon could already be present on the workspace. To make sure
             // the icon state is updated, we send a change event.
             callback.onPackageStateChanged(PackageInstallInfo.fromInstallingState(sessionInfo));
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 8708d5a..032de31 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -28,9 +28,13 @@
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.UserBadgeDrawable;
+import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -166,4 +170,14 @@
     public List<UserHandle> getUserProfiles() {
         return List.copyOf(mUserToSerialMap.keySet());
     }
+
+    /**
+     * Get a non-themed {@link UserBadgeDrawable} based on the provided {@link UserHandle}.
+     */
+    @Nullable
+    public static UserBadgeDrawable getBadgeDrawable(Context context, UserHandle userHandle) {
+        return (UserBadgeDrawable) BitmapInfo.LOW_RES_INFO.withFlags(UserCache.getInstance(context)
+                        .getUserInfo(userHandle).applyBitmapInfoFlags(FlagOp.NO_OP))
+                .getBadgeDrawable(context, false /* isThemed */);
+    }
 }
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index c9c5fd3..1c9db17 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
@@ -44,6 +45,7 @@
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
@@ -53,6 +55,7 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
@@ -204,17 +207,21 @@
                 .collect(Collectors.toList());
         container = (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
                 R.layout.popup_container, launcher.getDragLayer(), false);
-        container.configureForLauncher(launcher);
+        container.configureForLauncher(launcher, item);
         container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts);
         launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
         container.requestFocus();
         return container;
     }
 
-    private void configureForLauncher(Launcher launcher) {
+    private void configureForLauncher(Launcher launcher, ItemInfo itemInfo) {
         addOnAttachStateChangeListener(new LauncherPopupLiveUpdateHandler(
                 launcher, (PopupContainerWithArrow<Launcher>) this));
-        mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
+        if (!Flags.privateSpaceRestrictItemDrag()
+                || !(itemInfo instanceof ItemInfoWithIcon itemInfoWithIcon)
+                || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0) {
+            mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
+        }
         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
         launcher.getDragController().addDragListener(this);
     }
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 5f17959..f1d837c 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -31,8 +31,10 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -41,6 +43,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -218,6 +221,32 @@
                 .collect(Collectors.toList());
     }
 
+    /** Returns the recommended widgets mapped by their category. */
+    public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
+        Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
+                .filter(entry -> entry instanceof WidgetsListContentEntry)
+                .flatMap(entry -> entry.mWidgets.stream())
+                .distinct()
+                .collect(Collectors.toMap(
+                        widget -> new ComponentKey(widget.componentName, widget.user),
+                        Function.identity()
+                ));
+        return mRecommendedWidgets.stream()
+                .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo)
+                .collect(Collectors.groupingBy(
+                        it -> ((PendingAddWidgetInfo) it).recommendationCategory,
+                        Collectors.collectingAndThen(
+                                Collectors.toList(),
+                                list -> list.stream()
+                                        .map(it -> allWidgetItems.get(
+                                                new ComponentKey(it.getTargetComponent(),
+                                                        it.user)))
+                                        .filter(Objects::nonNull)
+                                        .collect(Collectors.toList())
+                        )
+                ));
+    }
+
     public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
         return mAllWidgets.stream()
                 .filter(row -> row instanceof WidgetsListContentEntry
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java
index 8d1d96b..79b25a4 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.secondarydisplay;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -129,6 +131,10 @@
         dragView.show(mLastTouch.x, mLastTouch.y);
         mDistanceSinceScroll = 0;
 
+        if (!isItemPinnable()) {
+            MAIN_EXECUTOR.post(this:: cancelDrag);
+        }
+
         if (!mIsInPreDrag) {
             callOnDragStart();
         } else if (mOptions.preDragCondition != null) {
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index f24529a..1623ad8 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -152,6 +152,12 @@
                 }, this::getCurrentActivity);
             }
 
+            case TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT: {
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        mDeviceProfile.cellLayoutBorderSpacePx.y);
+                return response;
+            }
+
             case TestProtocol.REQUEST_SYSTEM_GESTURE_REGION: {
                 return getUIProperty(Bundle::putParcelable, activity -> {
                     WindowInsetsCompat insets = WindowInsetsCompat.toWindowInsetsCompat(
diff --git a/src/com/android/launcher3/touch/DefaultPagedViewHandler.java b/src/com/android/launcher3/touch/DefaultPagedViewHandler.java
new file mode 100644
index 0000000..272ed10
--- /dev/null
+++ b/src/com/android/launcher3/touch/DefaultPagedViewHandler.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 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.touch;
+
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.launcher3.Utilities;
+
+public class DefaultPagedViewHandler implements PagedOrientationHandler {
+    @Override
+    public int getPrimaryValue(int x, int y) {
+        return x;
+    }
+
+    @Override
+    public int getSecondaryValue(int x, int y) {
+        return y;
+    }
+
+    @Override
+    public float getPrimaryValue(float x, float y) {
+        return x;
+    }
+
+    @Override
+    public float getSecondaryValue(float x, float y) {
+        return y;
+    }
+
+    @Override
+    public <T> void setPrimary(T target, Int2DAction<T> action, int param) {
+        action.call(target, param, 0);
+    }
+
+    @Override
+    public <T> void setPrimary(T target, Float2DAction<T> action, float param) {
+        action.call(target, param, 0);
+    }
+
+    @Override
+    public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
+        return event.getX(pointerIndex);
+    }
+
+    @Override
+    public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) {
+        return velocityTracker.getXVelocity(pointerId);
+    }
+
+    @Override
+    public int getMeasuredSize(View view) {
+        return view.getMeasuredWidth();
+    }
+
+    @Override
+    public int getPrimaryScroll(View view) {
+        return view.getScrollX();
+    }
+
+    @Override
+    public float getPrimaryScale(View view) {
+        return view.getScaleX();
+    }
+
+    @Override
+    public void setMaxScroll(AccessibilityEvent event, int maxScroll) {
+        event.setMaxScrollX(maxScroll);
+    }
+
+    @Override
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return !Utilities.isRtl(resources);
+    }
+
+    @Override
+    public int getChildStart(View view) {
+        return view.getLeft();
+    }
+
+    @Override
+    public int getCenterForPage(View view, Rect insets) {
+        return (view.getPaddingTop() + view.getMeasuredHeight() + insets.top
+            - insets.bottom - view.getPaddingBottom()) / 2;
+    }
+
+    @Override
+    public int getScrollOffsetStart(View view, Rect insets) {
+        return insets.left + view.getPaddingLeft();
+    }
+
+    @Override
+    public int getScrollOffsetEnd(View view, Rect insets) {
+        return view.getWidth() - view.getPaddingRight() - insets.right;
+    }
+
+    @Override
+    public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
+            boolean layoutChild) {
+        final int childWidth = child.getMeasuredWidth();
+        final int childRight = childStart + childWidth;
+        final int childHeight = child.getMeasuredHeight();
+        final int childTop = pageCenter - childHeight / 2;
+        if (layoutChild) {
+            child.layout(childStart, childTop, childRight, childTop + childHeight);
+        }
+        return new ChildBounds(childWidth, childHeight, childRight, childTop);
+    }
+
+}
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index f47ab90..ded4da6 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.touch;
 
-import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_BIND_PENDING_APPWIDGET;
 import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
@@ -331,7 +330,7 @@
 
         // Check for abandoned promise
         if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()
-                && (!enableSupportForArchiving() || !shortcut.isArchived())) {
+                && (!Utilities.enableSupportForArchiving() || !shortcut.isArchived())) {
             String packageName = shortcut.getIntent().getComponent() != null
                     ? shortcut.getIntent().getComponent().getPackageName()
                     : shortcut.getIntent().getPackage();
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 74d88ba..e0c4e3c 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -19,26 +19,11 @@
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
-import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.ShapeDrawable;
-import android.util.FloatProperty;
-import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.util.SplitConfigurationOptions;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
-import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
-
-import java.util.List;
 
 /**
  * Abstraction layer to separate horizontal and vertical specific implementations
@@ -47,9 +32,7 @@
  */
 public interface PagedOrientationHandler {
 
-    PagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler();
-    PagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler();
-    PagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler();
+    PagedOrientationHandler DEFAULT = new DefaultPagedViewHandler();
 
     interface Int2DAction<T> {
         void call(T target, int x, int y);
@@ -64,39 +47,18 @@
 
     <T> void setPrimary(T target, Int2DAction<T> action, int param);
     <T> void setPrimary(T target, Float2DAction<T> action, float param);
-    <T> void setSecondary(T target, Float2DAction<T> action, float param);
-    <T> void set(T target, Int2DAction<T> action, int primaryParam, int secondaryParam);
     float getPrimaryDirection(MotionEvent event, int pointerIndex);
     float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
     int getMeasuredSize(View view);
-    int getPrimarySize(View view);
-    float getPrimarySize(RectF rect);
-    float getStart(RectF rect);
-    float getEnd(RectF rect);
-    int getClearAllSidePadding(View view, boolean isRtl);
-    int getSecondaryDimension(View view);
-    FloatProperty<View> getPrimaryViewTranslate();
-    FloatProperty<View> getSecondaryViewTranslate();
-
     int getPrimaryScroll(View view);
     float getPrimaryScale(View view);
     int getChildStart(View view);
     int getCenterForPage(View view, Rect insets);
     int getScrollOffsetStart(View view, Rect insets);
     int getScrollOffsetEnd(View view, Rect insets);
-    int getSecondaryTranslationDirectionFactor();
-    int getSplitTranslationDirectionFactor(@StagePosition int stagePosition,
-            DeviceProfile deviceProfile);
     ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
     void setMaxScroll(AccessibilityEvent event, int maxScroll);
     boolean getRecentsRtlSetting(Resources resources);
-    float getDegreesRotated();
-    int getRotation();
-    void setPrimaryScale(View view, float scale);
-    void setSecondaryScale(View view, float scale);
-
-    <T> T getPrimaryValue(T x, T y);
-    <T> T getSecondaryValue(T x, T y);
 
     int getPrimaryValue(int x, int y);
     int getSecondaryValue(int x, int y);
@@ -104,174 +66,6 @@
     float getPrimaryValue(float x, float y);
     float getSecondaryValue(float x, float y);
 
-    boolean isLayoutNaturalToLauncher();
-    Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
-            FloatProperty secondary, DeviceProfile deviceProfile);
-    int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
-    List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
-    /**
-     * @param placeholderHeight height of placeholder view in portrait, width in landscape
-     */
-    void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset,
-            DeviceProfile dp, @StagePosition int stagePosition, Rect out);
-
-    /**
-     * Centers an icon in the split staging area, accounting for insets.
-     * @param out The icon that needs to be centered.
-     * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is
-     *                        offscreen).
-     * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is
-     *                        offscreen).
-     * @param fullscreenScaleX A x-scaling factor used to convert coordinates back into pixels.
-     * @param fullscreenScaleY A y-scaling factor used to convert coordinates back into pixels.
-     * @param drawableWidth The icon's drawable (final) width.
-     * @param drawableHeight The icon's drawable (final) height.
-     * @param dp The device profile, used to report rotation and hardware insets.
-     * @param stagePosition 0 if the staging area is pinned to top/left, 1 for bottom/right.
-     */
-    void updateSplitIconParams(View out, float onScreenRectCenterX,
-            float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY,
-            int drawableWidth, int drawableHeight, DeviceProfile dp,
-            @StagePosition int stagePosition);
-
-    /**
-     * Sets positioning and rotation for a SplitInstructionsView.
-     * @param out The SplitInstructionsView that needs to be positioned.
-     * @param dp The device profile, used to report rotation and device type.
-     * @param splitInstructionsHeight The SplitInstructionView's height.
-     * @param splitInstructionsWidth  The SplitInstructionView's width.
-     */
-    void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
-            int splitInstructionsWidth);
-
-    /**
-     * @param splitDividerSize height of split screen drag handle in portrait, width in landscape
-     * @param stagePosition the split position option (top/left, bottom/right) of the first
-     *                           task selected for entering split
-     * @param out1 the bounds for where the first selected app will be
-     * @param out2 the bounds for where the second selected app will be, complimentary to
-     *             {@param out1} based on {@param initialSplitOption}
-     */
-    void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
-            @StagePosition int stagePosition, Rect out1, Rect out2);
-
-    int getDefaultSplitPosition(DeviceProfile deviceProfile);
-
-    /**
-     * @param outRect This is expected to be the rect that has the dimensions for a non-split,
-     *                fullscreen task in overview. This will directly be modified.
-     * @param desiredStagePosition Which stage position (topLeft/rightBottom) we want to resize
-     *                           outRect for
-     */
-    void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo,
-            @SplitConfigurationOptions.StagePosition int desiredStagePosition);
-
-    void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
-            int parentWidth, int parentHeight,
-            SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl);
-
-    // Overview TaskMenuView methods
-    void setTaskIconParams(FrameLayout.LayoutParams iconParams,
-            int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl);
-    void setIconAppChipMenuParams(View iconAppChipMenuView, FrameLayout.LayoutParams iconMenuParams,
-            int iconMenuMargin, int thumbnailTopMargin);
-    void setSplitIconParams(View primaryIconView, View secondaryIconView,
-            int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
-            int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
-            DeviceProfile deviceProfile, SplitBounds splitConfig);
-
-    /*
-     * The following two methods try to center the TaskMenuView in landscape by finding the center
-     * of the thumbnail view and then subtracting half of the taskMenu width. In this case, the
-     * taskMenu width is the same size as the thumbnail width (what got set below in
-     * getTaskMenuWidth()), so we directly use that in the calculations.
-     */
-    float getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile,
-            float taskInsetMargin, View taskViewIcon);
-    float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin, View taskViewIcon);
-    int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
-            @StagePosition int stagePosition);
-    /**
-     * Sets linear layout orientation for {@link com.android.launcher3.popup.SystemShortcut} items
-     * inside task menu view.
-     */
-    void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
-            LinearLayout taskMenuLayout, int dividerSpacing,
-            ShapeDrawable dividerDrawable);
-    /**
-     * Sets layout param attributes for {@link com.android.launcher3.popup.SystemShortcut} child
-     * views inside task menu view.
-     */
-    void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
-            LinearLayout viewGroup, DeviceProfile deviceProfile);
-
-    /**
-     * Calculates the position where a Digital Wellbeing Banner should be placed on its parent
-     * TaskView.
-     * @return A Pair of Floats representing the proper x and y translations.
-     */
-    Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
-            int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
-            View[] thumbnailViews, int desiredTaskId, View banner);
-
-    // The following are only used by TaskViewTouchHandler.
-    /** @return Either VERTICAL or HORIZONTAL. */
-    SingleAxisSwipeDetector.Direction getUpDownSwipeDirection();
-    /** @return Given {@link #getUpDownSwipeDirection()}, whether POSITIVE or NEGATIVE is up. */
-    int getUpDirection(boolean isRtl);
-    /** @return Whether the displacement is going towards the top of the screen. */
-    boolean isGoingUp(float displacement, boolean isRtl);
-    /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
-    int getTaskDragDisplacementFactor(boolean isRtl);
-
-    /**
-     * Maps the velocity from the coordinate plane of the foreground app to that
-     * of Launcher's (which now will always be portrait)
-     */
-    void adjustFloatingIconStartVelocity(PointF velocity);
-
-    /**
-     * Ensures that outStartRect left bound is within the DeviceProfile's visual boundaries
-     * @param outStartRect The start rect that will directly be modified
-     */
-    void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile);
-
-    /**
-     * Determine the target translation for animating the FloatingTaskView out. This value could
-     * either be an x-coordinate or a y-coordinate, depending on which way the FloatingTaskView was
-     * docked.
-     *
-     * @param floatingTask The FloatingTaskView.
-     * @param onScreenRect The current on-screen dimensions of the FloatingTaskView.
-     * @param stagePosition STAGE_POSITION_TOP_OR_LEFT or STAGE_POSITION_BOTTOM_OR_RIGHT.
-     * @param dp The device profile.
-     * @return A float. When an animation translates the FloatingTaskView to this position, it will
-     * appear to tuck away off the edge of the screen.
-     */
-    float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect,
-            @StagePosition int stagePosition, DeviceProfile dp);
-
-    /**
-     * Sets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
-     * either x or y), depending on how the view is oriented.
-     *
-     * @param floatingTask The FloatingTaskView to be translated.
-     * @param translation The target translation value.
-     * @param dp The current device profile.
-     */
-    void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, DeviceProfile dp);
-
-    /**
-     * Gets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
-     * either x or y), depending on how the view is oriented.
-     *
-     * @param floatingTask The FloatingTaskView in question.
-     * @param dp The current device profile.
-     * @return The current translation value.
-     */
-    Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp);
-
     class ChildBounds {
 
         public final int primaryDimension;
@@ -279,8 +73,8 @@
         public final int childPrimaryEnd;
         public final int childSecondaryEnd;
 
-        ChildBounds(int primaryDimension, int secondaryDimension, int childPrimaryEnd,
-            int childSecondaryEnd) {
+        public ChildBounds(int primaryDimension, int secondaryDimension, int childPrimaryEnd,
+                int childSecondaryEnd) {
             this.primaryDimension = primaryDimension;
             this.secondaryDimension = secondaryDimension;
             this.childPrimaryEnd = childPrimaryEnd;
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 1419dc4..ff95212 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -362,10 +362,10 @@
                 WindowManagerProxy wmProxy,
                 Map<CachedDisplayInfo, List<WindowBounds>> perDisplayBoundsCache) {
             CachedDisplayInfo displayInfo = wmProxy.getDisplayInfo(displayInfoContext);
-            normalizedDisplayInfo = displayInfo.normalize();
+            normalizedDisplayInfo = displayInfo.normalize(wmProxy);
             rotation = displayInfo.rotation;
             currentSize = displayInfo.size;
-            cutout = displayInfo.cutout;
+            cutout = WindowManagerProxy.getSafeInsets(displayInfo.cutout);
 
             Configuration config = displayInfoContext.getResources().getConfiguration();
             fontScale = config.fontScale;
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 2b5aaf5..50d8886 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.util;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
+
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Context;
@@ -112,8 +114,7 @@
             @NonNull final UserHandle user, final int flags) {
         try {
             ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
-            return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
-                    ? null : info;
+            return !isPackageInstalledOrArchived(info) || !info.enabled ? null : info;
         } catch (PackageManager.NameNotFoundException e) {
             return null;
         }
@@ -253,4 +254,11 @@
         }
         return 100;
     }
+
+    /** Returns true in case app is installed on the device or in archived state. */
+    @SuppressWarnings("NewApi")
+    private boolean isPackageInstalledOrArchived(ApplicationInfo info) {
+        return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 || (
+                enableSupportForArchiving() && info.isArchived);
+    }
 }
diff --git a/src/com/android/launcher3/util/window/CachedDisplayInfo.java b/src/com/android/launcher3/util/window/CachedDisplayInfo.java
index 23f37aa..c5084ad 100644
--- a/src/com/android/launcher3/util/window/CachedDisplayInfo.java
+++ b/src/com/android/launcher3/util/window/CachedDisplayInfo.java
@@ -16,13 +16,16 @@
 package com.android.launcher3.util.window;
 
 import static com.android.launcher3.util.RotationUtils.deltaRotation;
-import static com.android.launcher3.util.RotationUtils.rotateRect;
 import static com.android.launcher3.util.RotationUtils.rotateSize;
 
+import android.graphics.Insets;
 import android.graphics.Point;
-import android.graphics.Rect;
+import android.view.DisplayCutout;
 import android.view.Surface;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.Objects;
 
 /**
@@ -30,36 +33,40 @@
  */
 public class CachedDisplayInfo {
 
+    private static final DisplayCutout NO_CUTOUT =
+            new DisplayCutout(Insets.NONE, null, null, null, null);
+
     public final Point size;
     public final int rotation;
-    public final Rect cutout;
+    @NonNull
+    public final DisplayCutout cutout;
 
     public CachedDisplayInfo() {
         this(new Point(0, 0), 0);
     }
 
     public CachedDisplayInfo(Point size, int rotation) {
-        this(size, rotation, new Rect());
+        this(size, rotation, NO_CUTOUT);
     }
 
-    public CachedDisplayInfo(Point size, int rotation, Rect cutout) {
+    public CachedDisplayInfo(Point size, int rotation, @Nullable DisplayCutout cutout) {
         this.size = size;
         this.rotation = rotation;
-        this.cutout = cutout;
+        this.cutout = cutout == null ? NO_CUTOUT : cutout;
     }
 
     /**
      * Returns a CachedDisplayInfo where the properties are normalized to {@link Surface#ROTATION_0}
      */
-    public CachedDisplayInfo normalize() {
+    public CachedDisplayInfo normalize(WindowManagerProxy windowManagerProxy) {
         if (rotation == Surface.ROTATION_0) {
             return this;
         }
         Point newSize = new Point(size);
         rotateSize(newSize, deltaRotation(rotation, Surface.ROTATION_0));
 
-        Rect newCutout = new Rect(cutout);
-        rotateRect(newCutout, deltaRotation(rotation, Surface.ROTATION_0));
+        DisplayCutout newCutout = windowManagerProxy.rotateCutout(
+                cutout, size.x, size.y, rotation, Surface.ROTATION_0);
         return new CachedDisplayInfo(newSize, Surface.ROTATION_0, newCutout);
     }
 
@@ -79,11 +86,16 @@
         CachedDisplayInfo that = (CachedDisplayInfo) o;
         return rotation == that.rotation
                 && Objects.equals(size, that.size)
-                && Objects.equals(cutout, that.cutout);
+                && cutout.getSafeInsetLeft() == that.cutout.getSafeInsetLeft()
+                && cutout.getSafeInsetTop() == that.cutout.getSafeInsetTop()
+                && cutout.getSafeInsetRight() == that.cutout.getSafeInsetRight()
+                && cutout.getSafeInsetBottom() == that.cutout.getSafeInsetBottom();
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(size, rotation, cutout);
+        return Objects.hash(size, rotation,
+                cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
+                cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
     }
 }
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 209a3dc..32f1736 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -95,7 +95,7 @@
      */
     public ArrayMap<CachedDisplayInfo, List<WindowBounds>> estimateInternalDisplayBounds(
             Context displayInfoContext) {
-        CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize();
+        CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize(this);
         List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
         ArrayMap<CachedDisplayInfo, List<WindowBounds>> result = new ArrayMap<>();
         result.put(info, bounds);
@@ -186,10 +186,9 @@
      * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations
      */
     protected List<WindowBounds> estimateWindowBounds(Context context,
-            CachedDisplayInfo displayInfo) {
+            final CachedDisplayInfo displayInfo) {
         int densityDpi = context.getResources().getConfiguration().densityDpi;
-        int rotation = displayInfo.rotation;
-        Rect safeCutout = displayInfo.cutout;
+        final int rotation = displayInfo.rotation;
 
         int minSize = Math.min(displayInfo.size.x, displayInfo.size.y);
         int swDp = (int) dpiFromPx(minSize, densityDpi);
@@ -247,8 +246,9 @@
                 statusBarHeight = statusBarHeightLandscape;
             }
 
-            Rect insets = new Rect(safeCutout);
-            rotateRect(insets, rotationChange);
+            DisplayCutout rotatedCutout = rotateCutout(
+                    displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i);
+            Rect insets = getSafeInsets(rotatedCutout);
             insets.top = Math.max(insets.top, statusBarHeight);
             insets.bottom = Math.max(insets.bottom, navBarHeight);
 
@@ -298,8 +298,7 @@
             Point size = new Point();
             Display display = getDisplay(displayInfoContext);
             display.getRealSize(size);
-            Rect cutoutRect = new Rect();
-            return new CachedDisplayInfo(size, rotation, cutoutRect);
+            return new CachedDisplayInfo(size, rotation);
         }
     }
 
@@ -309,13 +308,8 @@
     @TargetApi(Build.VERSION_CODES.S)
     protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) {
         Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom);
-        Rect cutoutRect = new Rect();
-        DisplayCutout cutout = windowMetrics.getWindowInsets().getDisplayCutout();
-        if (cutout != null) {
-            cutoutRect.set(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
-                    cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
-        }
-        return new CachedDisplayInfo(size, rotation, cutoutRect);
+        return new CachedDisplayInfo(size, rotation,
+                windowMetrics.getWindowInsets().getDisplayCutout());
     }
 
     /**
@@ -355,6 +349,16 @@
     }
 
     /**
+     * Returns a DisplayCutout which represents a rotated version of the original
+     */
+    protected DisplayCutout rotateCutout(DisplayCutout original, int startWidth, int startHeight,
+            int fromRotation, int toRotation) {
+        Rect safeCutout = getSafeInsets(original);
+        rotateRect(safeCutout, deltaRotation(fromRotation, toRotation));
+        return new DisplayCutout(Insets.of(safeCutout), null, null, null, null);
+    }
+
+    /**
      * Returns the current navigation mode from resource.
      */
     public NavigationMode getNavigationMode(Context context) {
@@ -373,4 +377,12 @@
         return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON :
                 NavigationMode.THREE_BUTTONS;
     }
+
+    /**
+     * @see DisplayCutout#getSafeInsets
+     */
+    public static Rect getSafeInsets(DisplayCutout cutout) {
+        return new Rect(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
+                cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
+    }
 }
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index c5317e3..230a651 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -155,6 +155,19 @@
     }
 
     /**
+     * Handle user tapping on unsupported target when in split selection mode.
+     * See {@link #isSplitSelectionActive()}
+     *
+     * @return {@code true} if this method will handle the incorrect target selection,
+     *         {@code false} if it could not be handled or if not possible to handle based on
+     *         current split state
+     */
+    default boolean handleIncorrectSplitTargetSelection() {
+        // Overridden
+        return false;
+    }
+
+    /**
      * The root view to support drag-and-drop and popup support.
      */
     BaseDragLayer getDragLayer();
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
index 76b6fde..c60e1a4 100644
--- a/src/com/android/launcher3/views/FloatingSurfaceView.java
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -175,7 +175,6 @@
 
             if (!mTmpPosition.equals(mIconPosition)) {
                 mIconPosition.set(mTmpPosition);
-                sendIconInfo();
 
                 LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
                 lp.width = Math.round(mIconPosition.width());
@@ -184,6 +183,9 @@
                 lp.topMargin = Math.round(mIconPosition.top);
             }
         }
+
+        sendIconInfo();
+
         if (mIcon != null && iconChanged && !mIconBounds.isEmpty()) {
             // Record the icon display
             setCurrentIconVisible(true);
@@ -197,7 +199,7 @@
     }
 
     private void sendIconInfo() {
-        if (mContract != null && !mIconPosition.isEmpty()) {
+        if (mContract != null) {
             mContract.sendEndPosition(mIconPosition, mLauncher, mSurfaceView.getSurfaceControl());
         }
     }
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 9de7f62..145ad80 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -72,14 +72,21 @@
 
     public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mContentHorizontalMargin = getResources().getDimensionPixelSize(
-                R.dimen.widget_list_horizontal_margin);
+        mContentHorizontalMargin = getWidgetListHorizontalMargin();
         mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize(
                 R.dimen.widget_cell_horizontal_padding);
         mNavBarScrimPaint = new Paint();
         mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext));
     }
 
+    /**
+     * Returns the margins to be applied to the left and right of the widget apps list.
+     */
+    protected int getWidgetListHorizontalMargin() {
+        return getResources().getDimensionPixelSize(
+                R.dimen.widget_list_horizontal_margin);
+    }
+
     protected int getScrimColor(Context context) {
         return context.getResources().getColor(R.color.widgets_picker_scrim);
     }
@@ -142,8 +149,7 @@
     @Override
     public void setInsets(Rect insets) {
         mInsets.set(insets);
-        @Px int contentHorizontalMargin = getResources().getDimensionPixelSize(
-                R.dimen.widget_list_horizontal_margin);
+        @Px int contentHorizontalMargin = getWidgetListHorizontalMargin();
         if (contentHorizontalMargin != mContentHorizontalMargin) {
             onContentHorizontalMarginChanged(contentHorizontalMargin);
             mContentHorizontalMargin = contentHorizontalMargin;
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index ccf4b2e..a501960 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory;
 import com.android.launcher3.widget.util.WidgetSizes;
 
 /**
@@ -42,6 +43,16 @@
     public Bundle bindOptions = null;
     public int sourceContainer;
 
+    public WidgetRecommendationCategory recommendationCategory = null;
+
+    public PendingAddWidgetInfo(
+            LauncherAppWidgetProviderInfo i,
+            int container,
+            WidgetRecommendationCategory recommendationCategory) {
+        this(i, container);
+        this.recommendationCategory = recommendationCategory;
+    }
+
     public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, int container) {
         if (i.isCustomWidget()) {
             itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
diff --git a/src/com/android/launcher3/widget/picker/OWNERS b/src/com/android/launcher3/widget/picker/OWNERS
new file mode 100644
index 0000000..6aabbfa
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/OWNERS
@@ -0,0 +1,16 @@
+set noparent
+
+# Bug component: 1481801
+
+# People who can approve changes for submission
+#
+
+# Widget Picker OWNERS
+zakcohen@google.com
+shamalip@google.com
+wvk@google.com
+
+# Launcher OWNERS
+captaincole@google.com
+sunnygoyal@google.com
+
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
index d99c5d1..801b1f6 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
@@ -80,6 +80,28 @@
     /** Maps application category to an appropriate displayable category. */
     private static WidgetRecommendationCategory getCategoryFromApplicationCategory(
             Context context, int applicationCategory, String componentName) {
+        // Weather categories don't map to a specific application category, so, we maintain an
+        // allowlist.
+        String[] weatherRecommendationAllowlist =
+                context.getResources().getStringArray(R.array.weather_recommendations);
+        for (String allowedWeatherComponentName : weatherRecommendationAllowlist) {
+            if (componentName.equalsIgnoreCase(allowedWeatherComponentName)) {
+                return new WidgetRecommendationCategory(
+                        R.string.weather_widget_recommendation_category_label, /*order=*/3);
+            }
+        }
+
+        // Fitness categories don't map to a specific application category, so, we maintain an
+        // allowlist.
+        String[] fitnessRecommendationAllowlist =
+                context.getResources().getStringArray(R.array.fitness_recommendations);
+        for (String allowedFitnessComponentName : fitnessRecommendationAllowlist) {
+            if (componentName.equalsIgnoreCase(allowedFitnessComponentName)) {
+                return new WidgetRecommendationCategory(
+                        R.string.fitness_widget_recommendation_category_label, /*order=*/2);
+            }
+        }
+
         if (applicationCategory == ApplicationInfo.CATEGORY_PRODUCTIVITY) {
             return new WidgetRecommendationCategory(
                     R.string.productivity_widget_recommendation_category_label, /*order=*/0);
@@ -99,26 +121,6 @@
                     /*order=*/4);
         }
 
-        // Fitness & weather categories don't map to a specific application category, so, we
-        // maintain an allowlist.
-        String[] weatherRecommendationAllowlist =
-                context.getResources().getStringArray(R.array.weather_recommendations);
-        for (String allowedWeatherComponentName : weatherRecommendationAllowlist) {
-            if (componentName.equalsIgnoreCase(allowedWeatherComponentName)) {
-                return new WidgetRecommendationCategory(
-                        R.string.weather_widget_recommendation_category_label, /*order=*/2);
-            }
-        }
-
-        String[] fitnessRecommendationAllowlist =
-                context.getResources().getStringArray(R.array.fitness_recommendations);
-        for (String allowedFitnessComponentName : fitnessRecommendationAllowlist) {
-            if (componentName.equalsIgnoreCase(allowedFitnessComponentName)) {
-                return new WidgetRecommendationCategory(
-                        R.string.fitness_widget_recommendation_category_label, /*order=*/3);
-            }
-        }
-
         return new WidgetRecommendationCategory(
                 R.string.others_widget_recommendation_category_label, /*order=*/5);
     }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index e9a590b..17d9276 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -182,7 +182,8 @@
                 .stream()
                 .anyMatch(user -> mUserCache.getUserInfo(user).isWork());
         mWorkWidgetsFilter = entry -> mHasWorkProfile
-                && mUserCache.getUserInfo(entry.mPkgItem.user).isWork();
+                && mUserCache.getUserInfo(entry.mPkgItem.user).isWork()
+                && !mUserManagerState.isUserQuiet(entry.mPkgItem.user);
         mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
         mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
         mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 6656237..26c04f5 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -303,6 +303,12 @@
     }
 
     @Override
+    protected int getWidgetListHorizontalMargin() {
+        return getResources().getDimensionPixelSize(
+                R.dimen.widget_list_left_pane_horizontal_margin);
+    }
+
+    @Override
     protected boolean isTwoPane() {
         return true;
     }
diff --git a/tests/Android.bp b/tests/Android.bp
index 310e418..ed8609e 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -176,6 +176,7 @@
     name: "launcher-testing-shared",
     srcs: [
         "multivalentTests/shared/com/android/launcher3/testing/shared/**/*.java",
+        "multivalentTests/shared/com/android/launcher3/testing/shared/**/*.kt"
     ],
     resource_dirs: [],
     manifest: "multivalentTests/shared/AndroidManifest.xml",
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 eefdbd4..8d40ff2 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -121,6 +121,7 @@
     public static final String REQUEST_IS_TABLET = "is-tablet";
     public static final String REQUEST_NUM_ALL_APPS_COLUMNS = "num-all-apps-columns";
     public static final String REQUEST_IS_TWO_PANELS = "is-two-panel";
+    public static final String REQUEST_CELL_LAYOUT_BOARDER_HEIGHT = "cell-layout-boarder-height";
     public static final String REQUEST_START_DRAG_THRESHOLD = "start-drag-threshold";
     public static final String REQUEST_SHELL_DRAG_READY = "shell-drag-ready";
     public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT =
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
index dbbdcf5..62f2259 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
@@ -66,7 +66,7 @@
     }
 
     public CellLayoutBoard(int width, int height) {
-        mWidget = new char[width + 1][height + 1];
+        mWidget = new char[width][height];
         this.mWidth = width;
         this.mHeight = height;
         for (int x = 0; x < mWidget.length; x++) {
@@ -371,8 +371,8 @@
         s.append("\n");
         maxX = Math.min(maxX, mWidget.length);
         maxY = Math.min(maxY, mWidget[0].length);
-        for (int y = 0; y <= maxY; y++) {
-            for (int x = 0; x <= maxX; x++) {
+        for (int y = 0; y < maxY; y++) {
+            for (int x = 0; x < maxX; x++) {
                 s.append(mWidget[x][y]);
             }
             s.append('\n');
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
index 770024f..fcfb3db 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
@@ -27,7 +27,7 @@
      *   usually less than 100.
      * @return a randomly generated board filled with icons and widgets.
      */
-    open fun generateBoard(width: Int, height: Int, remainingEmptySpaces: Int): CellLayoutBoard? {
+    open fun generateBoard(width: Int, height: Int, remainingEmptySpaces: Int): CellLayoutBoard {
         val cellLayoutBoard = CellLayoutBoard(width, height)
         return fillBoard(cellLayoutBoard, Rect(0, 0, width, height), remainingEmptySpaces)
     }
@@ -39,8 +39,8 @@
     ): CellLayoutBoard {
         var remainingEmptySpaces = remainingEmptySpacesArg
         if (area.height() * area.width() <= 0) return board
-        val width = getRandom(1, area.width() - 1)
-        val height = getRandom(1, area.height() - 1)
+        val width = getRandom(1, area.width())
+        val height = getRandom(1, area.height())
         val x = area.left + getRandom(0, area.width() - width)
         val y = area.top + getRandom(0, area.height() - height)
         if (remainingEmptySpaces > 0) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 3a54915..8eebdb2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/multivalentTests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -44,7 +44,9 @@
             @Override
             public void evaluate() throws Throwable {
                 try {
+                    // we expect to begin unlocked...
                     AbstractLauncherUiTest.verifyKeyguardInvisible();
+
                     mTest.mDevice.pressHome();
                     mTest.waitForLauncherCondition("Launcher activity wasn't created",
                             launcher -> launcher != null);
@@ -67,6 +69,9 @@
                         }
                     });
                     mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
+
+                    // and end unlocked...
+                    AbstractLauncherUiTest.verifyKeyguardInvisible();
                 }
             }
 
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/Background.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/Background.java
index 9f2ce22..988aa94 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.tapl.BaseOverview.TASK_RES_ID;
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
 
@@ -116,10 +117,10 @@
                         // non-tablet overview, snapshots can be on either side of the swiped
                         // task, but we still check that they become visible after swiping and
                         // pausing.
-                        mLauncher.waitForOverviewObject("snapshot");
+                        mLauncher.waitForOverviewObject(TASK_RES_ID);
                         if (mLauncher.isTablet()) {
                             List<UiObject2> tasks = mLauncher.getDevice().findObjects(
-                                    mLauncher.getOverviewObjectSelector("snapshot"));
+                                    mLauncher.getOverviewObjectSelector(TASK_RES_ID));
                             final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
                             mLauncher.assertTrue(
                                     "All tasks not to the left of the swiped task",
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 1bc489c..ad95ecf 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -19,9 +19,11 @@
 import static android.view.KeyEvent.KEYCODE_ESCAPE;
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.graphics.Rect;
+import android.view.KeyEvent;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -42,11 +44,16 @@
  * Common overview panel for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
+    protected static final String TASK_RES_ID = "task";
 
     private static final Pattern EVENT_ALT_ESC_DOWN = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
     private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+    private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ENTER");
+    private static final Pattern EVENT_ENTER_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ENTER");
 
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
 
@@ -275,10 +282,10 @@
 
         // The widest, and most top-right task should be the current task
         UiObject2 currentTask = Collections.max(taskViews,
-                Comparator.comparingInt((UiObject2 t) -> t.getParent().getVisibleBounds().width())
-                        .thenComparingInt((UiObject2 t) -> t.getParent().getVisibleCenter().x)
+                Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width())
+                        .thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x)
                         .thenComparing(Comparator.comparing(
-                                (UiObject2 t) -> t.getParent().getVisibleCenter().y).reversed()));
+                                (UiObject2 t) -> t.getVisibleCenter().y).reversed()));
         return new OverviewTask(mLauncher, currentTask, this);
     }
 
@@ -323,10 +330,11 @@
                 "want to get overview tasks")) {
             verifyActiveContainer();
             return mLauncher.getDevice().findObjects(
-                    mLauncher.getOverviewObjectSelector("snapshot"));
+                    mLauncher.getOverviewObjectSelector(TASK_RES_ID));
         }
     }
 
+
     int getTaskCount() {
         return getTasks().size();
     }
@@ -414,6 +422,31 @@
         }
     }
 
+    /**
+     * Presses the enter key to launch the focused task
+     * <p>
+     * If no task is focused, this will fail.
+     */
+    public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_DOWN);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+
+            mLauncher.executeAndWaitForLauncherStop(
+                    () -> mLauncher.assertTrue(
+                            "Failed to press enter",
+                            mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)),
+                    "pressing enter");
+            mLauncher.assertAppLaunched(expectedPackageName);
+
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "pressed enter")) {
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
     private void verifyActionsViewVisibility() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to assert overview actions view visibility")) {
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 5466d08..cdde605 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -407,6 +407,11 @@
                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    int getCellLayoutBoarderHeight() {
+        return getTestInfo(TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     int getFocusedTaskHeightForTablet() {
         return getTestInfo(TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET).getInt(
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/OverviewTask.java
index f383e99..afe5722 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -16,6 +16,10 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.DEFAULT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT;
+
 import android.graphics.Rect;
 
 import androidx.annotation.NonNull;
@@ -34,9 +38,6 @@
  */
 public final class OverviewTask {
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
-    private static final String TASK_SNAPSHOT_1 = "snapshot";
-    private static final String TASK_SNAPSHOT_2 = "bottomright_snapshot";
-
     static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync");
     static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect");
     static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks");
@@ -64,15 +65,16 @@
             return getCombinedSplitTaskHeight();
         }
 
-        return mTask.getVisibleBounds().height();
+        UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
+        return taskSnapshot1.getVisibleBounds().height();
     }
 
     /**
      * Calculates the visible height for split tasks, containing 2 snapshot tiles and a divider.
      */
     private int getCombinedSplitTaskHeight() {
-        UiObject2 taskSnapshot1 = findObjectInTask(TASK_SNAPSHOT_1);
-        UiObject2 taskSnapshot2 = findObjectInTask(TASK_SNAPSHOT_2);
+        UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes);
+        UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes);
 
         // If the split task is partly off screen, taskSnapshot1 can be invisible.
         if (taskSnapshot1 == null) {
@@ -96,15 +98,16 @@
             return getCombinedSplitTaskWidth();
         }
 
-        return mTask.getVisibleBounds().width();
+        UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
+        return taskSnapshot1.getVisibleBounds().width();
     }
 
     /**
      * Calculates the visible width for split tasks, containing 2 snapshot tiles and a divider.
      */
     private int getCombinedSplitTaskWidth() {
-        UiObject2 taskSnapshot1 = findObjectInTask(TASK_SNAPSHOT_1);
-        UiObject2 taskSnapshot2 = findObjectInTask(TASK_SNAPSHOT_2);
+        UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes);
+        UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes);
 
         int left = Math.min(
                 taskSnapshot1.getVisibleBounds().left, taskSnapshot2.getVisibleBounds().left);
@@ -115,15 +118,15 @@
     }
 
     int getTaskCenterX() {
-        return mTask.getParent().getVisibleCenter().x;
+        return mTask.getVisibleCenter().x;
     }
 
     int getTaskCenterY() {
-        return mTask.getParent().getVisibleCenter().y;
+        return mTask.getVisibleCenter().y;
     }
 
     float getExactCenterX() {
-        return mTask.getParent().getVisibleBounds().exactCenterX();
+        return mTask.getVisibleBounds().exactCenterX();
     }
 
     UiObject2 getUiObject() {
@@ -225,11 +228,17 @@
     /** Taps the task menu. Returns the task menu object. */
     @NonNull
     public OverviewTaskMenu tapMenu() {
+        return tapMenu(DEFAULT);
+    }
+
+    /** Taps the task menu of the split task. Returns the split task's menu object. */
+    @NonNull
+    public OverviewTaskMenu tapMenu(OverviewSplitTask task) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "want to tap the task menu")) {
             mLauncher.clickLauncherObject(
-                    mLauncher.waitForObjectInContainer(mTask.getParent(), "icon"));
+                    mLauncher.waitForObjectInContainer(mTask, task.iconAppRes));
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "tapped the task menu")) {
@@ -238,27 +247,31 @@
         }
     }
 
-    /** Taps the task menu of the split task. Returns the split task's menu object. */
-    @NonNull
-    public OverviewTaskMenu tapSplitTaskMenu() {
-        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
-             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                     "want to tap the split task's menu")) {
-            mLauncher.clickLauncherObject(
-                    mLauncher.waitForObjectInContainer(mTask.getParent(), "bottomRight_icon"));
-
-            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
-                    "tapped the split task's menu")) {
-                return new OverviewTaskMenu(mLauncher);
-            }
-        }
-    }
-
     boolean isTaskSplit() {
-        return findObjectInTask(TASK_SNAPSHOT_2) != null;
+        return findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes) != null;
     }
 
     private UiObject2 findObjectInTask(String resName) {
-        return mTask.getParent().findObject(mLauncher.getOverviewObjectSelector(resName));
+        return mTask.findObject(mLauncher.getOverviewObjectSelector(resName));
+    }
+
+    /**
+     * Enum used to specify  which task is retrieved when it is a split task.
+     */
+    public enum OverviewSplitTask {
+        // The main task when the task is not split.
+        DEFAULT("snapshot", "icon"),
+        // The first task in split task.
+        SPLIT_TOP_OR_LEFT("snapshot", "icon"),
+        // The second task in split task.
+        SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon");
+
+        public final String snapshotRes;
+        public final String iconAppRes;
+
+        OverviewSplitTask(String snapshotRes, String iconAppRes) {
+            this.snapshotRes = snapshotRes;
+            this.iconAppRes = iconAppRes;
+        }
     }
 }
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
index d0573e0..3895302 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
@@ -57,7 +57,8 @@
             Rect originalWidgetSize = widget.getVisibleBounds();
             Point targetStart = bottomResizeHandle.getVisibleCenter();
             Point targetDest = bottomResizeHandle.getVisibleCenter();
-            targetDest.offset(0, originalWidgetSize.height());
+            targetDest.offset(0,
+                    originalWidgetSize.height() + mLauncher.getCellLayoutBoarderHeight());
 
             final long downTime = SystemClock.uptimeMillis();
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetStart,
diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index d44ccf5..dbafe79 100644
--- a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -60,8 +60,8 @@
     private val windowManagerProxy: WindowManagerProxy = mock()
     private val launcherPrefs: LauncherPrefs = mock()
     private val allowLeftRightSplitInPortrait: Boolean = initAllowLeftRightSplitInPortrait()
-    fun initAllowLeftRightSplitInPortrait() : Boolean {
-        val res = Resources.getSystem();
+    fun initAllowLeftRightSplitInPortrait(): Boolean {
+        val res = Resources.getSystem()
         val resId = res.getIdentifier("config_leftRightSplitInPortrait", "bool", "android")
         return Flags.enableLeftRightSplitInPortrait() && resId > 0 && res.getBoolean(resId)
     }
@@ -124,8 +124,7 @@
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
-        val displayInfo =
-            CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+        val displayInfo = CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0)
         val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
 
         initializeCommonVars(
@@ -144,8 +143,7 @@
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
-        val displayInfo =
-            CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+        val displayInfo = CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0)
         val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
 
         initializeCommonVars(
@@ -168,21 +166,13 @@
         val unfoldedWindowsBounds =
             tabletWindowsBounds(deviceSpecUnfolded, unfoldedNaturalX, unfoldedNaturalY)
         val unfoldedDisplayInfo =
-            CachedDisplayInfo(
-                Point(unfoldedNaturalX, unfoldedNaturalY),
-                Surface.ROTATION_0,
-                Rect(0, 0, 0, 0)
-            )
+            CachedDisplayInfo(Point(unfoldedNaturalX, unfoldedNaturalY), Surface.ROTATION_0)
 
         val (foldedNaturalX, foldedNaturalY) = deviceSpecFolded.naturalSize
         val foldedWindowsBounds =
             phoneWindowsBounds(deviceSpecFolded, isGestureMode, foldedNaturalX, foldedNaturalY)
         val foldedDisplayInfo =
-            CachedDisplayInfo(
-                Point(foldedNaturalX, foldedNaturalY),
-                Surface.ROTATION_0,
-                Rect(0, 0, 0, 0)
-            )
+            CachedDisplayInfo(Point(foldedNaturalX, foldedNaturalY), Surface.ROTATION_0)
 
         val perDisplayBoundsCache =
             mapOf(
@@ -325,12 +315,16 @@
         // TODO(b/315230497): We don't currently have device-specific device profile dumps, so just
         //  update the result before we do the comparison
         if (allowLeftRightSplitInPortrait) {
-            val isLeftRightSplitInPortrait = when {
-                allowLeftRightSplitInPortrait && dp.isTablet -> !dp.isLandscape
-                else -> dp.isLandscape
-            }
-            expected = expected.replace(Regex("isLeftRightSplit:\\w+"),
-                    "isLeftRightSplit:$isLeftRightSplitInPortrait")
+            val isLeftRightSplitInPortrait =
+                when {
+                    allowLeftRightSplitInPortrait && dp.isTablet -> !dp.isLandscape
+                    else -> dp.isLandscape
+                }
+            expected =
+                expected.replace(
+                    Regex("isLeftRightSplit:\\w+"),
+                    "isLeftRightSplit:$isLeftRightSplitInPortrait"
+                )
         }
 
         Truth.assertThat(dump).isEqualTo(expected)
diff --git a/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
index 27a2c75..ba74244 100644
--- a/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
@@ -62,5 +62,6 @@
         } finally {
             allApps.unfreeze();
         }
+        mLauncher.goHome();
     }
 }
diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
index da0beb1..6fce4c6 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.util.TestUtil.expectFail;
-import static com.android.launcher3.ui.AbstractLauncherUiTest.initialize;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -30,8 +29,8 @@
 import androidx.test.filters.FlakyTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
@@ -208,7 +207,7 @@
     public void testPressBackFromAllAppsToHome() {
         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                 READ_DEVICE_CONFIG_PERMISSION);
-        assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
+        assumeFalse(Flags.enablePredictiveBackGesture());
         mLauncher
                 .getWorkspace()
                 .switchToAllApps()
diff --git a/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt b/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
new file mode 100644
index 0000000..13dfd5e
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.celllayout
+
+import android.content.Context
+import android.graphics.Point
+import android.util.Log
+import android.view.View
+import androidx.core.view.get
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.CellLayout
+import com.android.launcher3.celllayout.board.CellLayoutBoard
+import com.android.launcher3.celllayout.board.IconPoint
+import com.android.launcher3.celllayout.board.PermutedBoardComparator
+import com.android.launcher3.celllayout.board.WidgetRect
+import com.android.launcher3.celllayout.testgenerator.RandomBoardGenerator
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.views.DoubleShadowBubbleTextView
+import java.util.Random
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+private class HotseatReorderTestCase(
+    val startBoard: CellLayoutBoard,
+    val endBoard: CellLayoutBoard
+) {
+    override fun toString(): String {
+        return "$startBoard#endBoard:\n$endBoard"
+    }
+}
+
+class HotseatReorderUnitTest {
+
+    private val applicationContext: Context =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+
+    @JvmField @Rule var cellLayoutBuilder = UnitTestCellLayoutBuilderRule()
+
+    /**
+     * This test generates random CellLayout configurations and then try to reorder it and makes
+     * sure the result is a valid board meaning it didn't remove any widget or icon.
+     */
+    @Test
+    fun generateValidTests() {
+        val generator = Random(Companion.SEED.toLong())
+        for (i in 0 until Companion.TOTAL_OF_CASES_GENERATED) {
+            // Using a new seed so that we can replicate the same test cases.
+            val seed = generator.nextInt()
+            Log.d(Companion.TAG, "Seed = $seed")
+
+            val testCase: HotseatReorderTestCase =
+                generateRandomTestCase(RandomBoardGenerator(Random(seed.toLong())))
+            Log.d(Companion.TAG, "testCase = $testCase")
+
+            Assert.assertTrue(
+                "invalid case $i",
+                PermutedBoardComparator().compare(testCase.startBoard, testCase.endBoard) == 0
+            )
+        }
+    }
+
+    private fun addViewInCellLayout(
+        cellLayout: CellLayout,
+        cellX: Int,
+        cellY: Int,
+        spanX: Int,
+        spanY: Int,
+        isWidget: Boolean
+    ) {
+        val cell =
+            if (isWidget) View(applicationContext)
+            else DoubleShadowBubbleTextView(applicationContext)
+        cell.layoutParams = CellLayoutLayoutParams(cellX, cellY, spanX, spanY)
+        cellLayout.addViewToCellLayout(
+            cell,
+            -1,
+            cell.id,
+            cell.layoutParams as CellLayoutLayoutParams,
+            true
+        )
+    }
+
+    private fun solve(board: CellLayoutBoard): CellLayout {
+        val cl = cellLayoutBuilder.createCellLayout(board.width, board.height, false)
+        // The views have to be sorted or the result can vary
+        board.icons
+            .map(IconPoint::getCoord)
+            .sortedWith(
+                Comparator.comparing { p: Any -> (p as Point).x }
+                    .thenComparing { p: Any -> (p as Point).y }
+            )
+            .forEach { p ->
+                addViewInCellLayout(
+                    cellLayout = cl,
+                    cellX = p.x,
+                    cellY = p.y,
+                    spanX = 1,
+                    spanY = 1,
+                    isWidget = false
+                )
+            }
+        board.widgets
+            .sortedWith(
+                Comparator.comparing(WidgetRect::getCellX).thenComparing(WidgetRect::getCellY)
+            )
+            .forEach { widget ->
+                addViewInCellLayout(
+                    cl,
+                    widget.cellX,
+                    widget.cellY,
+                    widget.spanX,
+                    widget.spanY,
+                    isWidget = true
+                )
+            }
+        if (cl.makeSpaceForHotseatMigration(true)) {
+            commitTempPosition(cl)
+        }
+        return cl
+    }
+
+    private fun commitTempPosition(cellLayout: CellLayout) {
+        val count = cellLayout.shortcutsAndWidgets.childCount
+        for (i in 0 until count) {
+            val params = cellLayout.shortcutsAndWidgets[i].layoutParams as CellLayoutLayoutParams
+            params.cellX = params.tmpCellX
+            params.cellY = params.tmpCellY
+        }
+    }
+
+    private fun boardFromCellLayout(cellLayout: CellLayout): CellLayoutBoard {
+        val views = mutableListOf<View>()
+        for (i in 0 until cellLayout.shortcutsAndWidgets.childCount) {
+            views.add(cellLayout.shortcutsAndWidgets.getChildAt(i))
+        }
+        return CellLayoutTestUtils.viewsToBoard(views, cellLayout.countX, cellLayout.countY)
+    }
+
+    private fun generateRandomTestCase(
+        boardGenerator: RandomBoardGenerator
+    ): HotseatReorderTestCase {
+        val width: Int = boardGenerator.getRandom(3, Companion.MAX_BOARD_SIZE)
+        val height: Int = boardGenerator.getRandom(3, Companion.MAX_BOARD_SIZE)
+        val targetWidth: Int = boardGenerator.getRandom(1, width - 2)
+        val targetHeight: Int = boardGenerator.getRandom(1, height - 2)
+        val board: CellLayoutBoard =
+            boardGenerator.generateBoard(width, height, targetWidth * targetHeight)
+        val finishBoard: CellLayoutBoard = boardFromCellLayout(solve(board))
+        return HotseatReorderTestCase(board, finishBoard)
+    }
+
+    companion object {
+        private const val MAX_BOARD_SIZE = 13
+
+        /**
+         * There is nothing special about this numbers, the random seed is just to be able to
+         * reproduce the test cases and the height and width is a random number similar to what
+         * users expect on their devices
+         */
+        private const val SEED = -194162315
+        private const val TOTAL_OF_CASES_GENERATED = 300
+        private const val TAG = "HotseatReorderUnitTest"
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
index d96287f..7aa26a1 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
@@ -161,11 +161,12 @@
 
         public int getWidgetId() throws InterruptedException {
             Intent intent = blockingGetExtraIntent();
-            assertNotNull(intent);
-            assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
+            assertNotNull("Null EXTRA_INTENT", intent);
+            assertEquals("Intent action is not ACTION_APPWIDGET_CONFIGURE",
+                    AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
             int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                     LauncherAppWidgetInfo.NO_ID);
-            assertNotSame(widgetId, LauncherAppWidgetInfo.NO_ID);
+            assertNotSame("Widget id is NO_ID", widgetId, LauncherAppWidgetInfo.NO_ID);
             return widgetId;
         }
     }
diff --git a/tests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 8670d40..706ab27 100644
--- a/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -95,8 +95,7 @@
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
 
         // Mock WindowManagerProxy
-        val displayInfo =
-            CachedDisplayInfo(Point(width, height), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+        val displayInfo = CachedDisplayInfo(Point(width, height), Surface.ROTATION_0)
         whenever(windowManagerProxy.getDisplayInfo(any())).thenReturn(displayInfo)
         whenever(windowManagerProxy.estimateInternalDisplayBounds(any()))
             .thenAnswer(
@@ -135,8 +134,7 @@
     @Test
     @UiThreadTest
     fun testRotation() {
-        val displayInfo =
-            CachedDisplayInfo(Point(height, width), Surface.ROTATION_90, Rect(0, 0, 0, 0))
+        val displayInfo = CachedDisplayInfo(Point(height, width), Surface.ROTATION_90)
         whenever(windowManagerProxy.getDisplayInfo(any())).thenReturn(displayInfo)
         whenever(display.rotation).thenReturn(displayInfo.rotation)
         val configuration =
diff --git a/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java
new file mode 100644
index 0000000..d1da5f4
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.util;
+
+import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link PackageManagerHelper}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class PackageManagerHelperTest {
+    @Rule
+    public ExpectedException exception = ExpectedException.none();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final String TEST_PACKAGE = "com.android.test.package";
+    private static final int TEST_USER = 2;
+
+    private Context mContext;
+    private LauncherApps mLauncherApps;
+    private PackageManagerHelper mPackageManagerHelper;
+
+    @Before
+    public void setup() {
+        mContext = mock(Context.class);
+        mLauncherApps = mock(LauncherApps.class);
+        when(mContext.getSystemService(eq(LauncherApps.class))).thenReturn(mLauncherApps);
+        mPackageManagerHelper = new PackageManagerHelper(mContext);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+    public void getApplicationInfo_archivedApp_appInfoIsNotNull()
+            throws PackageManager.NameNotFoundException {
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.isArchived = true;
+        when(mLauncherApps.getApplicationInfo(TEST_PACKAGE, 0 /* flags */,
+                UserHandle.of(TEST_USER)))
+                .thenReturn(applicationInfo);
+
+        assertThat(mPackageManagerHelper.getApplicationInfo(TEST_PACKAGE, UserHandle.of(TEST_USER),
+                0 /* flags */))
+                .isNotNull();
+    }
+}
diff --git a/tests/src/com/android/launcher3/widget/picker/OWNERS b/tests/src/com/android/launcher3/widget/picker/OWNERS
new file mode 100644
index 0000000..775b0c7
--- /dev/null
+++ b/tests/src/com/android/launcher3/widget/picker/OWNERS
@@ -0,0 +1,18 @@
+set noparent
+
+# Bug component: 1481801
+# People who can approve changes for submission
+#
+
+# Widget Picker OWNERS
+zakcohen@google.com
+shamalip@google.com
+wvk@google.com
+
+# For Tests
+vadimt@google.com
+
+# Launcher OWNERS
+captaincole@google.com
+sunnygoyal@google.com
+