Merge "Clear all DragView in onSaveInstanceState" into sc-v2-dev
diff --git a/Android.bp b/Android.bp
index 60ef5b1..bab994a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -258,6 +258,10 @@
         "go/res",
         "go/quickstep/res",
     ],
+    // Note the ordering here is important when it comes to resource
+    // overriding. We want the most specific resource overrides defined
+    // in QuickstepResLib to take precendece, so it should be the final
+    // dependency. See b/205278434 for how this can go wrong.
     static_libs: [
         "Launcher3CommonDepsLib",
         "QuickstepResLib",
@@ -283,11 +287,15 @@
     libs: [
         "framework-statsd",
     ],
+    // Note the ordering here is important when it comes to resource
+    // overriding. We want the most specific resource overrides defined
+    // in QuickstepResLib to take precendece, so it should be the final
+    // dependency. See b/208647810 for how this can go wrong.
     static_libs: [
-        "QuickstepResLib",
         "SystemUI-statsd",
         "SystemUISharedLib",
         "Launcher3CommonDepsLib",
+        "QuickstepResLib",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     platform_apis: true,
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index d1bf152..c559988 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -60,6 +60,7 @@
     PredictedHotseatContainer predicted_hotseat_container = 10;
     TaskSwitcherContainer task_switcher_container = 11;
     TaskBarContainer task_bar_container = 12;
+    WallpapersContainer wallpapers_container = 13;
     ExtendedContainers extended_containers = 20;
   }
 }
@@ -247,6 +248,12 @@
   }
 }
 
+// Represents wallpapers container for quick switching.
+message WallpapersContainer {
+  // Number of wallpapers in the container.
+  optional int32 cardinality = 1;
+}
+
 // Represents state of EditText field before update.
 enum FromState {
   // Default value.
diff --git a/quickstep/res/drawable/ic_screenshot.xml b/quickstep/res/drawable/ic_screenshot.xml
index d97eae1..9ee6c44 100644
--- a/quickstep/res/drawable/ic_screenshot.xml
+++ b/quickstep/res/drawable/ic_screenshot.xml
@@ -13,11 +13,20 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
   <path
-      android:fillColor="#FF000000"
-      android:pathData="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,21L7,21v-1h10v1zM17,18L7,18L7,6h10v12zM17,4L7,4L7,3h10v1zM9.5,8.5L12,8.5L12,7L8,7v4h1.5zM12,17h4v-4h-1.5v2.5L12,15.5z"/>
-</vector>
+      android:pathData="M5.8334,1.666H8.3334V3.3327H5.8334V6.666H4.1667V3.3327C4.1667,2.4122 4.9129,1.666 5.8334,1.666Z"
+      android:fillColor="#000000"/>
+  <path
+      android:pathData="M4.1667,13.3327V16.666C4.1667,17.5865 4.9129,18.3327 5.8334,18.3327H8.3334V16.666H5.8334V13.3327H4.1667Z"
+      android:fillColor="#000000"/>
+  <path
+      android:pathData="M14.1667,13.3327V16.666H11.6667V18.3327H14.1667C15.0872,18.3327 15.8334,17.5865 15.8334,16.666V13.3327H14.1667Z"
+      android:fillColor="#000000"/>
+  <path
+      android:pathData="M15.8334,6.666V3.3327C15.8334,2.4122 15.0872,1.666 14.1667,1.666H11.6667V3.3327H14.1667V6.666H15.8334Z"
+      android:fillColor="#000000"/>
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index dd8afc2..0fda0bf 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -38,6 +38,20 @@
             android:layout_height="wrap_content"
             android:drawableStart="@drawable/ic_screenshot"
             android:text="@string/action_screenshot"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+        <Space
+            android:id="@+id/action_split_space"
+            android:layout_width="@dimen/overview_actions_button_spacing"
+            android:layout_height="1dp"
+            android:visibility="gone" />
+
+        <Button
+            android:id="@+id/action_split"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/action_split"
             android:theme="@style/ThemeControlHighlightWorkspaceColor"
             android:visibility="gone" />
 
@@ -46,23 +60,6 @@
             android:layout_height="1dp"
             android:layout_weight="1" />
 
-        <Button
-            android:id="@+id/action_split"
-            style="@style/OverviewActionButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:drawableStart="@drawable/ic_split_screen"
-            android:text="@string/action_split"
-            android:theme="@style/ThemeControlHighlightWorkspaceColor"
-            android:visibility="gone" />
-
-        <Space
-            android:id="@+id/action_split_space"
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_weight="1"
-            android:visibility="gone" />
-
         <Space
             android:id="@+id/oav_three_button_space"
             android:layout_width="0dp"
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index f303f31..01d675f 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -25,13 +25,6 @@
         android:clipToPadding="false"
         android:visibility="invisible" />
 
-    <com.android.quickstep.views.SplitPlaceholderView
-        android:id="@+id/split_placeholder"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/split_placeholder_size"
-        android:background="@android:color/darker_gray"
-        android:visibility="gone" />
-
     <include
         android:id="@+id/overview_actions_view"
         layout="@layout/overview_actions_container" />
diff --git a/quickstep/res/values-sw720dp/dimens.xml b/quickstep/res/values-sw720dp/dimens.xml
new file mode 100644
index 0000000..2831a6f
--- /dev/null
+++ b/quickstep/res/values-sw720dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (c) 2021, 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.
+*/
+-->
+<resources>
+    <dimen name="overview_grid_row_spacing">44dp</dimen>
+    <dimen name="overview_page_spacing_grid_portrait">44dp</dimen>
+    <dimen name="overview_page_spacing_grid_landscape">44dp</dimen>
+</resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 4e6b7b9..5ea94e9 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -36,9 +36,12 @@
     <dimen name="overview_task_margin">16dp</dimen>
     <dimen name="overview_task_margin_focused">12dp</dimen>
     <dimen name="overview_task_margin_grid">4dp</dimen>
+    <item name="overview_max_scale" format="float" type="dimen">0.7</item>
 
     <!-- Overrideable in overlay that provides the Overview Actions. -->
     <dimen name="overview_actions_height">48dp</dimen>
+    <dimen name="overview_actions_button_spacing">32dp</dimen>
+    <dimen name="overview_actions_button_spacing_grid">36dp</dimen>
     <dimen name="overview_actions_margin_gesture">28dp</dimen>
     <dimen name="overview_actions_top_margin_gesture_grid_portrait">19.37dp</dimen>
     <dimen name="overview_actions_bottom_margin_gesture_grid_portrait">22dp</dimen>
@@ -49,9 +52,10 @@
 
     <dimen name="overview_grid_side_margin_portrait">60dp</dimen>
     <dimen name="overview_grid_side_margin_landscape">52dp</dimen>
-    <dimen name="overview_grid_row_spacing_portrait">17.13dp</dimen>
-    <dimen name="overview_grid_row_spacing_landscape">13.38dp</dimen>
-    <dimen name="overview_grid_focus_vertical_margin">0dp</dimen>
+    <dimen name="overview_grid_row_spacing">36dp</dimen>
+    <dimen name="overview_page_spacing">16dp</dimen>
+    <dimen name="overview_page_spacing_grid_portrait">36dp</dimen>
+    <dimen name="overview_page_spacing_grid_landscape">38dp</dimen>
 
     <!-- These speeds are in dp/s -->
     <dimen name="max_task_dismiss_drag_velocity">2.25dp</dimen>
@@ -59,8 +63,6 @@
     <dimen name="default_task_dismiss_drag_velocity_grid">1dp</dimen>
     <dimen name="default_task_dismiss_drag_velocity_grid_focus_task">5dp</dimen>
 
-    <dimen name="recents_page_spacing">16dp</dimen>
-    <dimen name="recents_page_spacing_grid">36dp</dimen>
     <dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
 
     <!-- The speed in dp/s at which the user needs to be scrolling in recents such that we start
@@ -214,6 +216,7 @@
 
     <!-- Taskbar -->
     <dimen name="taskbar_size">@*android:dimen/taskbar_frame_height</dimen>
+    <dimen name="taskbar_ime_size">48dp</dimen>
     <dimen name="taskbar_icon_touch_size">48dp</dimen>
     <dimen name="taskbar_icon_drag_icon_size">54dp</dimen>
     <dimen name="taskbar_folder_margin">16dp</dimen>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 151b8e4..a4bbae0 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -218,10 +218,10 @@
     <string name="taskbar_edu_closed">Taskbar education closed</string>
     <!-- Text in dialog that lets a user know how they can use the taskbar to switch apps on their device.
          [CHAR_LIMIT=60] -->
-    <string name="taskbar_edu_switch_apps" translatable="false">Use the taskbar to switch apps</string>
+    <string name="taskbar_edu_switch_apps">Use the taskbar to switch apps</string>
     <!-- Text in dialog that lets a user know how they can use the taskbar to use multiple apps at once on their device.
          [CHAR_LIMIT=60] -->
-    <string name="taskbar_edu_splitscreen" translatable="false">Drag to the side to use two apps at once</string>
+    <string name="taskbar_edu_splitscreen">Drag to the side to use two apps at once</string>
     <!-- Text in dialog that lets a user know how they can hide the taskbar on their device.
          [CHAR_LIMIT=60] -->
     <string name="taskbar_edu_stashing">Touch &amp; hold to hide the taskbar</string>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 51d7914..0123c4f 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1214,14 +1214,14 @@
     }
 
     /**
-     * Returns view on the workspace that corresponds to the closing app in the list of app targets
+     * Returns view on launcher that corresponds to the closing app in the list of app targets
      */
-    private @Nullable View findWorkspaceView(RemoteAnimationTargetCompat[] appTargets) {
+    private @Nullable View findLauncherView(RemoteAnimationTargetCompat[] appTargets) {
         for (RemoteAnimationTargetCompat appTarget : appTargets) {
             if (appTarget.mode == MODE_CLOSING) {
-                View workspaceView = findWorkspaceView(appTarget);
-                if (workspaceView != null) {
-                    return workspaceView;
+                View launcherView = findLauncherView(appTarget);
+                if (launcherView != null) {
+                    return launcherView;
                 }
             }
         }
@@ -1229,9 +1229,9 @@
     }
 
     /**
-     * Returns view on the workspace that corresponds to the {@param runningTaskTarget}.
+     * Returns view on launcher that corresponds to the {@param runningTaskTarget}.
      */
-    private @Nullable View findWorkspaceView(RemoteAnimationTargetCompat runningTaskTarget) {
+    private @Nullable View findLauncherView(RemoteAnimationTargetCompat runningTaskTarget) {
         if (runningTaskTarget == null || runningTaskTarget.taskInfo == null) {
             return null;
         }
@@ -1269,7 +1269,7 @@
             }
         }
 
-        return mLauncher.getWorkspace().getFirstMatchForAppClose(launchCookieItemId,
+        return mLauncher.getFirstMatchForAppClose(launchCookieItemId,
                 packageName, UserHandle.of(runningTaskTarget.taskInfo.userId));
     }
 
@@ -1292,7 +1292,7 @@
      * Closing animator that animates the window into its final location on the workspace.
      */
     private void getClosingWindowAnimators(AnimatorSet animation,
-            RemoteAnimationTargetCompat[] targets, View workspaceView) {
+            RemoteAnimationTargetCompat[] targets, View launcherView, PointF velocityPxPerS) {
         FloatingIconView floatingIconView = null;
         FloatingWidgetView floatingWidget = null;
         RectF targetRect = new RectF();
@@ -1308,17 +1308,17 @@
         }
 
         // Get floating view and target rect.
-        if (workspaceView instanceof LauncherAppWidgetHostView) {
+        if (launcherView instanceof LauncherAppWidgetHostView) {
             Size windowSize = new Size(mDeviceProfile.availableWidthPx,
                     mDeviceProfile.availableHeightPx);
             int fallbackBackgroundColor =
                     FloatingWidgetView.getDefaultBackgroundColor(mLauncher, runningTaskTarget);
             floatingWidget = FloatingWidgetView.getFloatingWidgetView(mLauncher,
-                    (LauncherAppWidgetHostView) workspaceView, targetRect, windowSize,
+                    (LauncherAppWidgetHostView) launcherView, targetRect, windowSize,
                     mDeviceProfile.isMultiWindowMode ? 0 : getWindowCornerRadius(mLauncher),
                     isTransluscent, fallbackBackgroundColor);
-        } else if (workspaceView != null) {
-            floatingIconView = getFloatingIconView(mLauncher, workspaceView,
+        } else if (launcherView != null) {
+            floatingIconView = getFloatingIconView(mLauncher, launcherView,
                     true /* hideOriginal */, targetRect, false /* isOpening */);
         } else {
             targetRect.set(getDefaultWindowTargetRect());
@@ -1373,15 +1373,10 @@
         }
 
         // Use a fixed velocity to start the animation.
-        float velocityPxPerS = DynamicResource.provider(mLauncher)
-                .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
-        PointF velocity = new PointF(0, -velocityPxPerS);
-        animation.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
-                true /* animateOverviewScrim */, workspaceView).getAnimators());
         animation.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
-                anim.start(mLauncher, velocity);
+                anim.start(mLauncher, velocityPxPerS);
             }
         });
     }
@@ -1556,22 +1551,30 @@
             if (anim == null) {
                 anim = new AnimatorSet();
 
-                View workspaceView = findWorkspaceView(appTargets);
-                boolean isWorkspaceViewVisible = workspaceView != null
-                        && !mLauncher.isInState(LauncherState.ALL_APPS)
-                        && !mLauncher.getWorkspace().isOverlayShown();
-                boolean playFallBackAnimation = !isWorkspaceViewVisible
-                        && (launcherIsATargetWithMode(appTargets, MODE_OPENING)
-                        || mLauncher.isForceInvisible());
+                final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible()
+                        || launcherIsATargetWithMode(appTargets, MODE_OPENING);
+
+                View launcherView = findLauncherView(appTargets);
+                boolean playFallBackAnimation = (launcherView == null
+                        && launcherIsForceInvisibleOrOpening)
+                        || mLauncher.getWorkspace().isOverlayShown();
 
                 boolean playWorkspaceReveal = true;
                 if (mFromUnlock) {
                     anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
                 } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get()
                         && !playFallBackAnimation) {
-                    getClosingWindowAnimators(anim, appTargets, workspaceView);
-                    // We play StaggeredWorkspaceAnim as a part of the closing window animation.
-                    playWorkspaceReveal = false;
+                    // Use a fixed velocity to start the animation.
+                    float velocityPxPerS = DynamicResource.provider(mLauncher)
+                            .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
+                    PointF velocity = new PointF(0, -velocityPxPerS);
+                    getClosingWindowAnimators(anim, appTargets, launcherView, velocity);
+                    if (!mLauncher.isInState(LauncherState.ALL_APPS)) {
+                        anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y,
+                                true /* animateOverviewScrim */, launcherView).getAnimators());
+                        // We play StaggeredWorkspaceAnim as a part of the closing window animation.
+                        playWorkspaceReveal = false;
+                    }
                 } else {
                     anim.play(getFallbackClosingWindowAnimators(appTargets));
                 }
@@ -1584,8 +1587,7 @@
                 // targets list because it is already visible). In that case, we force
                 // invisibility on touch down, and only reset it after the animation to home
                 // is initialized.
-                if (launcherIsATargetWithMode(appTargets, MODE_OPENING)
-                        || mLauncher.isForceInvisible()) {
+                if (launcherIsForceInvisibleOrOpening) {
                     addCujInstrumentation(
                             anim, InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
                     // Only register the content animation for cancellation when state changes
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index 90c035f..f1e6747 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -60,6 +60,7 @@
 
     @Override
     protected void onDestroy() {
+        super.onDestroy();
         mRecentsActivity.setTaskbarUIController(null);
         mRecentsActivity.getStateManager().removeStateListener(mStateListener);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 7d23439..2622700 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -99,6 +99,7 @@
 
     @Override
     protected void onDestroy() {
+        super.onDestroy();
         onLauncherResumedOrPaused(false);
         mTaskbarLauncherStateController.onDestroy();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 0ab7756..48b0f6d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -31,6 +31,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
@@ -86,6 +87,7 @@
     private static final int FLAG_DISABLE_RECENTS = 1 << 8;
     private static final int FLAG_DISABLE_BACK = 1 << 9;
     private static final int FLAG_NOTIFICATION_SHADE_EXPANDED = 1 << 10;
+    private static final int FLAG_SCREEN_PINNING_ACTIVE = 1 << 10;
 
     private static final int MASK_IME_SWITCHER_VISIBLE = FLAG_SWITCHER_SUPPORTED | FLAG_IME_VISIBLE;
 
@@ -104,6 +106,9 @@
 
     private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat(
             this::updateNavButtonTranslationY);
+    private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat(
+            this::updateNavButtonTranslationY);
+    // Only applies to mTaskbarNavButtonTranslationY
     private final AnimatedFloat mNavButtonTranslationYMultiplier = new AnimatedFloat(
             this::updateNavButtonTranslationY);
     private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat(
@@ -152,20 +157,34 @@
         mPropertyHolders.add(new StatePropertyHolder(
                 mControllers.taskbarViewController.getTaskbarIconAlpha()
                         .getProperty(ALPHA_INDEX_KEYGUARD),
-                flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, MultiValueAlpha.VALUE, 1, 0));
+                flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0
+                        && (flags & FLAG_SCREEN_PINNING_ACTIVE) == 0,
+                MultiValueAlpha.VALUE, 1, 0));
 
         mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
                 .getKeyguardBgTaskbar(),
                 flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, AnimatedFloat.VALUE, 1, 0));
 
-        // Make sure to remove nav bar buttons translation when notification shade is expanded.
-        mPropertyHolders.add(new StatePropertyHolder(mNavButtonTranslationYMultiplier,
-                flags -> (flags & FLAG_NOTIFICATION_SHADE_EXPANDED) != 0, AnimatedFloat.VALUE,
-                0, 1));
-
         // Force nav buttons (specifically back button) to be visible during setup wizard.
         boolean isInSetup = !mContext.isUserSetupComplete();
-        if (isThreeButtonNav || isInSetup) {
+        boolean alwaysShowButtons = isThreeButtonNav || isInSetup;
+
+        // Make sure to remove nav bar buttons translation when notification shade is expanded or
+        // IME is showing (add separate translation for IME).
+        int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE;
+        mPropertyHolders.add(new StatePropertyHolder(mNavButtonTranslationYMultiplier,
+                flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
+                0, 1));
+        // Center nav buttons in new height for IME.
+        float transForIme = (mContext.getDeviceProfile().taskbarSize
+                - mContext.getTaskbarHeightForIme()) / 2f;
+        // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
+        float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
+        mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
+                flags -> (flags & FLAG_IME_VISIBLE) != 0, AnimatedFloat.VALUE, transForIme,
+                defaultButtonTransY));
+
+        if (alwaysShowButtons) {
             initButtons(mNavButtonContainer, mEndContextualContainer,
                     mControllers.navButtonController);
 
@@ -286,6 +305,7 @@
         int shadeExpandedFlags = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
                 | SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
         boolean isNotificationShadeExpanded = (sysUiStateFlags & shadeExpandedFlags) != 0;
+        boolean isScreenPinningActive = (sysUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0;
 
         // TODO(b/202218289) we're getting IME as not visible on lockscreen from system
         updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible);
@@ -295,6 +315,7 @@
         updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled);
         updateStateForFlag(FLAG_DISABLE_BACK, isBackDisabled);
         updateStateForFlag(FLAG_NOTIFICATION_SHADE_EXPANDED, isNotificationShadeExpanded);
+        updateStateForFlag(FLAG_SCREEN_PINNING_ACTIVE, isScreenPinningActive);
 
         if (mA11yButton != null) {
             // Only used in 3 button
@@ -402,8 +423,10 @@
     }
 
     private void updateNavButtonTranslationY() {
-        mNavButtonsView.setTranslationY(mTaskbarNavButtonTranslationY.value
-                * mNavButtonTranslationYMultiplier.value);
+        float normalTranslationY = mTaskbarNavButtonTranslationY.value
+                * mNavButtonTranslationYMultiplier.value;
+        float otherTranslationY = mTaskbarNavButtonTranslationYForIme.value;
+        mNavButtonsView.setTranslationY(normalTranslationY + otherTranslationY);
     }
 
     private void updateNavButtonDarkIntensity() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index cb9d4a4..73c6579 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -32,6 +32,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo.Config;
 import android.content.pm.LauncherApps;
+import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
@@ -96,6 +97,7 @@
 
     private final WindowManager mWindowManager;
     private final @Nullable RoundedCorner mLeftCorner, mRightCorner;
+    private final int mTaskbarHeightForIme;
     private WindowManager.LayoutParams mWindowLayoutParams;
     private boolean mIsFullscreen;
     // The size we should return to when we call setTaskbarWindowFullscreen(false)
@@ -122,10 +124,13 @@
         mIsUserSetupComplete = SettingsCache.INSTANCE.get(this).getValue(
                 Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), 0);
 
-        float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
-        mDeviceProfile.updateIconSize(1, getResources());
+        final Resources resources = getResources();
+        float taskbarIconSize = resources.getDimension(R.dimen.taskbar_icon_size);
+        mDeviceProfile.updateIconSize(1, resources);
         float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
-        mDeviceProfile.updateIconSize(iconScale, getResources());
+        mDeviceProfile.updateIconSize(iconScale, resources);
+
+        mTaskbarHeightForIme = resources.getDimensionPixelSize(R.dimen.taskbar_ime_size);
 
         mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
 
@@ -197,7 +202,7 @@
         // Adjust the frame by the rounded corners (ie. leaving just the bar as the inset) when
         // the IME is showing
         mWindowLayoutParams.providedInternalImeInsets = Insets.of(0,
-                getDefaultTaskbarWindowHeight() - mDeviceProfile.taskbarSize, 0, 0);
+                getDefaultTaskbarWindowHeight() - mTaskbarHeightForIme, 0, 0);
 
         // Initialize controllers after all are constructed.
         mControllers.init(sharedState);
@@ -361,6 +366,7 @@
         mControllers.taskbarStashController.updateStateForSysuiFlags(systemUiStateFlags, fromInit);
         mControllers.taskbarScrimViewController.updateStateForSysuiFlags(systemUiStateFlags,
                 fromInit);
+        mControllers.navButtonController.updateSysuiFlags(systemUiStateFlags);
     }
 
     /**
@@ -422,7 +428,9 @@
         if (mWindowLayoutParams.height == height || mIsDestroyed) {
             return;
         }
-        if (height != MATCH_PARENT) {
+        if (height == MATCH_PARENT) {
+            height = mDeviceProfile.heightPx;
+        } else {
             mLastRequestedNonFullscreenHeight = height;
             if (mIsFullscreen) {
                 // We still need to be fullscreen, so defer any change to our height until we call
@@ -433,6 +441,8 @@
             }
         }
         mWindowLayoutParams.height = height;
+        mWindowLayoutParams.providedInternalImeInsets =
+                Insets.of(0, height - mTaskbarHeightForIme, 0, 0);
         mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams);
     }
 
@@ -443,6 +453,13 @@
         return mDeviceProfile.taskbarSize + Math.max(getLeftCornerRadius(), getRightCornerRadius());
     }
 
+    /**
+     * Returns the bottom insets taskbar provides to the IME when IME is visible.
+     */
+    public int getTaskbarHeightForIme() {
+        return mTaskbarHeightForIme;
+    }
+
     protected void onTaskbarIconClicked(View view) {
         Object tag = view.getTag();
         if (tag instanceof Task) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 152b255..593bfd8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.taskbar;
 
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
 import static com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION;
@@ -50,13 +49,15 @@
 
     public static final int FLAG_RESUMED = 1 << 0;
     public static final int FLAG_RECENTS_ANIMATION_RUNNING = 1 << 1;
-    public static final int FLAG_TRANSITION_STATE_START_STASHED = 1 << 2;
-    public static final int FLAG_TRANSITION_STATE_COMMITTED_STASHED = 1 << 3;
+    public static final int FLAG_TRANSITION_STATE_RUNNING = 1 << 2;
+
+    /** Equivalent to an int with all 1s for binary operation purposes */
+    private static final int FLAGS_ALL = ~0;
 
     private final AnimatedFloat mIconAlignmentForResumedState =
-            new AnimatedFloat(this::onIconAlignmentRatioChanged);
+            new AnimatedFloat(this::onIconAlignmentRatioChangedForAppAndHomeTransition);
     private final AnimatedFloat mIconAlignmentForGestureState =
-            new AnimatedFloat(this::onIconAlignmentRatioChanged);
+            new AnimatedFloat(this::onIconAlignmentRatioChangedForAppAndHomeTransition);
     private final AnimatedFloat mIconAlignmentForLauncherState =
             new AnimatedFloat(this::onIconAlignmentRatioChangedForStateTransition);
 
@@ -65,11 +66,9 @@
     private MultiValueAlpha.AlphaProperty mIconAlphaForHome;
     private BaseQuickstepLauncher mLauncher;
 
-    private int mPrevState;
+    private Integer mPrevState;
     private int mState;
-
-    private LauncherState mTargetStateOverride = null;
-    private LauncherState mTargetStateOverrideForStateTransition = null;
+    private LauncherState mLauncherState = LauncherState.NORMAL;
 
     private boolean mIsAnimatingToLauncherViaGesture;
     private boolean mIsAnimatingToLauncherViaResume;
@@ -79,16 +78,20 @@
 
                 @Override
                 public void onStateTransitionStart(LauncherState toState) {
-                    mTargetStateOverrideForStateTransition = toState;
-                    updateStateForFlag(FLAG_TRANSITION_STATE_START_STASHED,
-                            toState.isTaskbarStashed());
+                    if (toState != mLauncherState) {
+                        // Treat FLAG_TRANSITION_STATE_RUNNING as a changed flag even if a previous
+                        // state transition was already running, so we update the new target.
+                        mPrevState &= ~FLAG_TRANSITION_STATE_RUNNING;
+                        mLauncherState = toState;
+                    }
+                    updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, true);
                     applyState();
                 }
 
                 @Override
                 public void onStateTransitionComplete(LauncherState finalState) {
-                    updateStateForFlag(FLAG_TRANSITION_STATE_COMMITTED_STASHED,
-                            finalState.isTaskbarStashed());
+                    mLauncherState = finalState;
+                    updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, false);
                     applyState();
                 }
             };
@@ -105,7 +108,7 @@
                 (Consumer<Float>) alpha -> mLauncher.getHotseat().setIconsAlpha(alpha > 0 ? 0 : 1));
 
         mIconAlignmentForResumedState.finishAnimation();
-        onIconAlignmentRatioChanged();
+        onIconAlignmentRatioChangedForAppAndHomeTransition();
 
         mLauncher.getStateManager().addStateListener(mStateListener);
     }
@@ -126,26 +129,15 @@
         // If going home, align the icons to hotseat
         AnimatorSet animatorSet = new AnimatorSet();
 
+        // Update stashed flags first to ensure goingToUnstashedLauncherState() returns correctly.
         TaskbarStashController stashController = mControllers.taskbarStashController;
         stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE,
-                toState.isTaskbarStashed());
+                toState.isTaskbarStashed(mLauncher));
         stashController.updateStateForFlag(FLAG_IN_APP, false);
 
         updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, true);
         animatorSet.play(stashController.applyStateWithoutStart(duration));
         animatorSet.play(applyState(duration, false));
-        animatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animator) {
-                mTargetStateOverride = null;
-                animator.removeListener(this);
-            }
-
-            @Override
-            public void onAnimationStart(Animator animator) {
-                mTargetStateOverride = toState;
-            }
-        });
 
         TaskBarRecentsAnimationListener listener = new TaskBarRecentsAnimationListener(callbacks);
         callbacks.addListener(listener);
@@ -199,10 +191,11 @@
 
     public Animator applyState(long duration, boolean start) {
         Animator animator = null;
-        if (mPrevState != mState) {
-            int changedFlags = mPrevState ^ mState;
-            animator = onStateChangeApplied(changedFlags, duration, start);
+        if (mPrevState == null || mPrevState != mState) {
+            // If this is our initial state, treat all flags as changed.
+            int changedFlags = mPrevState == null ? FLAGS_ALL : mPrevState ^ mState;
             mPrevState = mState;
+            animator = onStateChangeApplied(changedFlags, duration, start);
         }
         return animator;
     }
@@ -212,7 +205,8 @@
         if (hasAnyFlag(changedFlags, FLAG_RESUMED)) {
             boolean isResumed = isResumed();
             ObjectAnimator anim = mIconAlignmentForResumedState
-                    .animateToValue(getCurrentIconAlignmentRatio(), isResumed ? 1 : 0)
+                    .animateToValue(isResumed && goingToUnstashedLauncherState()
+                            ? 1 : 0)
                     .setDuration(duration);
 
             anim.addListener(new AnimatorListenerAdapter() {
@@ -236,7 +230,8 @@
         if (hasAnyFlag(changedFlags, FLAG_RECENTS_ANIMATION_RUNNING)) {
             boolean isRecentsAnimationRunning = isRecentsAnimationRunning();
             Animator animator = mIconAlignmentForGestureState
-                    .animateToValue(isRecentsAnimationRunning ? 1 : 0);
+                    .animateToValue(isRecentsAnimationRunning && goingToUnstashedLauncherState()
+                            ? 1 : 0);
             if (isRecentsAnimationRunning) {
                 animator.setDuration(duration);
             }
@@ -254,14 +249,21 @@
             animatorSet.play(animator);
         }
 
-        if (hasAnyFlag(changedFlags, FLAG_TRANSITION_STATE_START_STASHED)) {
-            playStateTransitionAnim(isTransitionStateStartStashed(), animatorSet, duration,
-                    false /* committed */);
+        if (hasAnyFlag(changedFlags, FLAG_RESUMED | FLAG_RECENTS_ANIMATION_RUNNING)) {
+            boolean goingToLauncher = hasAnyFlag(FLAG_RESUMED | FLAG_RECENTS_ANIMATION_RUNNING);
+            animatorSet.play(mTaskbarBackgroundAlpha.animateToValue(goingToLauncher ? 0 : 1)
+                    .setDuration(duration));
         }
 
-        if (hasAnyFlag(changedFlags, FLAG_TRANSITION_STATE_COMMITTED_STASHED)) {
-            playStateTransitionAnim(isTransitionStateCommittedStashed(), animatorSet, duration,
-                    true /* committed */);
+        if (hasAnyFlag(changedFlags, FLAG_TRANSITION_STATE_RUNNING)) {
+            boolean committed = !hasAnyFlag(FLAG_TRANSITION_STATE_RUNNING);
+            playStateTransitionAnim(animatorSet, duration, committed);
+
+            if (committed && mLauncherState == LauncherState.QUICK_SWITCH) {
+                // We're about to be paused, set immediately to ensure seamless handoff.
+                updateStateForFlag(FLAG_RESUMED, false);
+                applyState(0 /* duration */);
+            }
         }
 
         if (start) {
@@ -270,35 +272,41 @@
         return animatorSet;
     }
 
-    private void playStateTransitionAnim(boolean isTransitionStateStashed,
-            AnimatorSet animatorSet, long duration, boolean committed) {
+    /** Returns whether we're going to a state where taskbar icons should align with launcher. */
+    private boolean goingToUnstashedLauncherState() {
+        return !mControllers.taskbarStashController.isInStashedLauncherState();
+    }
+
+    private void playStateTransitionAnim(AnimatorSet animatorSet, long duration,
+            boolean committed) {
+        boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
+        float toAlignment = mLauncherState.isTaskbarAlignedWithHotseat(mLauncher) ? 1 : 0;
+
         TaskbarStashController controller = mControllers.taskbarStashController;
-        controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE,
-                isTransitionStateStashed);
+        controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, isInStashedState);
         Animator stashAnimator = controller.applyStateWithoutStart(duration);
         if (stashAnimator != null) {
             stashAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    if (isTransitionStateStashed && committed) {
+                    if (isInStashedState && committed) {
                         // Reset hotseat alpha to default
                         mLauncher.getHotseat().setIconsAlpha(1);
                     }
-                    mTargetStateOverrideForStateTransition = null;
                 }
 
                 @Override
                 public void onAnimationStart(Animator animation) {
-                    mIconAlphaForHome.setValue(mLauncher.getHotseat().getIconsAlpha());
+                    if (mLauncher.getHotseat().getIconsAlpha() > 0) {
+                        mIconAlphaForHome.setValue(mLauncher.getHotseat().getIconsAlpha());
+                    }
                 }
             });
             animatorSet.play(stashAnimator);
-            animatorSet.play(mIconAlignmentForLauncherState.animateToValue(
-                    getCurrentIconAlignmentRatioForLauncherState(),
-                    isTransitionStateStashed ? 0 : 1));
-        } else {
-            mTargetStateOverrideForStateTransition = null;
         }
+
+        animatorSet.play(mIconAlignmentForLauncherState.animateToValue(toAlignment)
+                .setDuration(duration));
     }
 
     private boolean isResumed() {
@@ -309,29 +317,18 @@
         return (mState & FLAG_RECENTS_ANIMATION_RUNNING) != 0;
     }
 
-    private boolean isTransitionStateStartStashed() {
-        return (mState & FLAG_TRANSITION_STATE_START_STASHED) != 0;
-    }
-
-    private boolean isTransitionStateCommittedStashed() {
-        return (mState & FLAG_TRANSITION_STATE_COMMITTED_STASHED) != 0;
-    }
-
     private void onIconAlignmentRatioChangedForStateTransition() {
-        onIconAlignmentRatioChanged(
-                mTargetStateOverrideForStateTransition != null
-                        ? mTargetStateOverrideForStateTransition
-                        : mLauncher.getStateManager().getState(),
-                this::getCurrentIconAlignmentRatioForLauncherState);
+        if (!isResumed()) {
+            return;
+        }
+        onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatioForLauncherState);
     }
 
-    private void onIconAlignmentRatioChanged() {
-        onIconAlignmentRatioChanged(mTargetStateOverride != null ? mTargetStateOverride
-                : mLauncher.getStateManager().getState(), this::getCurrentIconAlignmentRatio);
+    private void onIconAlignmentRatioChangedForAppAndHomeTransition() {
+        onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatioBetweenAppAndHome);
     }
 
-    private void onIconAlignmentRatioChanged(LauncherState state,
-            Supplier<Float> alignmentSupplier) {
+    private void onIconAlignmentRatioChanged(Supplier<Float> alignmentSupplier) {
         if (mControllers == null) {
             return;
         }
@@ -339,12 +336,11 @@
         mControllers.taskbarViewController.setLauncherIconAlignment(
                 alignment, mLauncher.getDeviceProfile());
 
-        mTaskbarBackgroundAlpha.updateValue(1 - alignment);
-
-        setIconAlpha(state, alignment);
+        // Switch taskbar and hotseat in last frame
+        setTaskbarViewVisible(alignment < 1);
     }
 
-    private float getCurrentIconAlignmentRatio() {
+    private float getCurrentIconAlignmentRatioBetweenAppAndHome() {
         return Math.max(mIconAlignmentForResumedState.value, mIconAlignmentForGestureState.value);
     }
 
@@ -352,15 +348,6 @@
         return mIconAlignmentForLauncherState.value;
     }
 
-    private void setIconAlpha(LauncherState state, float progress) {
-        if ((state.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
-            // If the hotseat icons are visible, then switch taskbar in last frame
-            setTaskbarViewVisible(progress < 1);
-        } else {
-            mIconAlphaForHome.setValue(1 - progress);
-        }
-    }
-
     private void setTaskbarViewVisible(boolean isVisible) {
         mIconAlphaForHome.setValue(isVisible ? 1 : 0);
     }
@@ -385,9 +372,16 @@
 
         private void endGestureStateOverride(boolean finishedToApp) {
             mCallbacks.removeListener(this);
+
+            // Update the resumed state immediately to ensure a seamless handoff
+            boolean launcherResumed = !finishedToApp;
+            mIconAlignmentForResumedState.updateValue(launcherResumed ? 1 : 0);
+
             updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, false);
+            updateStateForFlag(FLAG_RESUMED, launcherResumed);
             applyState();
 
+
             TaskbarStashController controller = mControllers.taskbarStashController;
             controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
             controller.applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 56e9429..3cdcdf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -29,8 +29,8 @@
 import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
 import android.net.Uri;
+import android.os.Handler;
 import android.provider.Settings;
-import android.util.Log;
 import android.view.Display;
 
 import androidx.annotation.NonNull;
@@ -93,7 +93,8 @@
         Display display =
                 service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
         mContext = service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null);
-        mNavButtonController = new TaskbarNavButtonController(service);
+        mNavButtonController = new TaskbarNavButtonController(service,
+                SystemUiProxy.INSTANCE.get(mContext), new Handler());
         mUserSetupCompleteListener = isUserSetupComplete -> recreateTaskbar();
         mComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index ae23eda..d233365 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -19,8 +19,10 @@
 
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import android.os.Bundle;
+import android.os.Handler;
 
 import androidx.annotation.IntDef;
 
@@ -40,6 +42,13 @@
  */
 public class TaskbarNavButtonController {
 
+    /** Allow some time in between the long press for back and recents. */
+    static final int SCREEN_PIN_LONG_PRESS_THRESHOLD = 200;
+    static final int SCREEN_PIN_LONG_PRESS_RESET = SCREEN_PIN_LONG_PRESS_THRESHOLD + 100;
+
+    private long mLastScreenPinLongPress;
+    private boolean mScreenPinned;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
             BUTTON_BACK,
@@ -57,10 +66,20 @@
     static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1;
     static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1;
 
-    private final TouchInteractionService mService;
+    private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS;
+    private int mLongPressedButtons = 0;
 
-    public TaskbarNavButtonController(TouchInteractionService service) {
+    private final TouchInteractionService mService;
+    private final SystemUiProxy mSystemUiProxy;
+    private final Handler mHandler;
+
+    private final Runnable mResetLongPress = this::resetScreenUnpin;
+
+    public TaskbarNavButtonController(TouchInteractionService service,
+            SystemUiProxy systemUiProxy, Handler handler) {
         mService = service;
+        mSystemUiProxy = systemUiProxy;
+        mHandler = handler;
     }
 
     public void onButtonClick(@TaskbarButton int buttonType) {
@@ -72,13 +91,13 @@
                 navigateHome();
                 break;
             case BUTTON_RECENTS:
-                navigateToOverview();;
+                navigateToOverview();
                 break;
             case BUTTON_IME_SWITCH:
                 showIMESwitcher();
                 break;
             case BUTTON_A11Y:
-                notifyImeClick(false /* longClick */);
+                notifyA11yClick(false /* longClick */);
                 break;
         }
     }
@@ -89,46 +108,98 @@
                 startAssistant();
                 return true;
             case BUTTON_A11Y:
-                notifyImeClick(true /* longClick */);
+                notifyA11yClick(true /* longClick */);
                 return true;
             case BUTTON_BACK:
-            case BUTTON_IME_SWITCH:
             case BUTTON_RECENTS:
+                mLongPressedButtons |= buttonType;
+                return determineScreenUnpin();
+            case BUTTON_IME_SWITCH:
             default:
                 return false;
         }
     }
 
+    /**
+     * Checks if the user has long pressed back and recents buttons
+     * "together" (within {@link #SCREEN_PIN_LONG_PRESS_THRESHOLD})ms
+     * If so, then requests the system to turn off screen pinning.
+     *
+     * @return true if the long press is a valid user action in attempting to unpin an app
+     *         Will always return {@code false} when screen pinning is not active.
+     *         NOTE: Returning true does not mean that screen pinning has stopped
+     */
+    private boolean determineScreenUnpin() {
+        long timeNow = System.currentTimeMillis();
+        if (!mScreenPinned) {
+            return false;
+        }
+
+        if (mLastScreenPinLongPress == 0) {
+            // First button long press registered, just mark time and wait for second button press
+            mLastScreenPinLongPress = System.currentTimeMillis();
+            mHandler.postDelayed(mResetLongPress, SCREEN_PIN_LONG_PRESS_RESET);
+            return true;
+        }
+
+        if ((timeNow - mLastScreenPinLongPress) > SCREEN_PIN_LONG_PRESS_THRESHOLD) {
+            // Too long in-between presses, reset the clock
+            resetScreenUnpin();
+            return false;
+        }
+
+        if ((mLongPressedButtons & SCREEN_UNPIN_COMBO) == SCREEN_UNPIN_COMBO) {
+            // Hooray! They did it (finally...)
+            mSystemUiProxy.stopScreenPinning();
+            mHandler.removeCallbacks(mResetLongPress);
+            resetScreenUnpin();
+        }
+        return true;
+    }
+
+    private void resetScreenUnpin() {
+        mLongPressedButtons = 0;
+        mLastScreenPinLongPress = 0;
+    }
+
+    public void updateSysuiFlags(int sysuiFlags) {
+        mScreenPinned = (sysuiFlags & SYSUI_STATE_SCREEN_PINNING) != 0;
+    }
+
     private void navigateHome() {
         mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_HOME);
     }
 
     private void navigateToOverview() {
+        if (mScreenPinned) {
+            return;
+        }
         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
         mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE);
     }
 
     private void executeBack() {
-        SystemUiProxy.INSTANCE.getNoCreate().onBackPressed();
+        mSystemUiProxy.onBackPressed();
     }
 
     private void showIMESwitcher() {
-        SystemUiProxy.INSTANCE.getNoCreate().onImeSwitcherPressed();
+        mSystemUiProxy.onImeSwitcherPressed();
     }
 
-    private void notifyImeClick(boolean longClick) {
-        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
+    private void notifyA11yClick(boolean longClick) {
         if (longClick) {
-            systemUiProxy.notifyAccessibilityButtonLongClicked();
+            mSystemUiProxy.notifyAccessibilityButtonLongClicked();
         } else {
-            systemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId());
+            mSystemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId());
         }
     }
 
     private void startAssistant() {
+        if (mScreenPinned) {
+            return;
+        }
         Bundle args = new Bundle();
         args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
-        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
-        systemUiProxy.startAssistant(args);
+        mSystemUiProxy.startAssistant(args);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 8965dc4..a3ad835 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -216,6 +216,13 @@
         return hasAnyFlag(FLAGS_STASHED_IN_APP);
     }
 
+    /**
+     * Returns whether the taskbar should be stashed in the current LauncherState.
+     */
+    public boolean isInStashedLauncherState() {
+        return hasAnyFlag(FLAG_IN_STASHED_LAUNCHER_STATE) && supportsVisualStashing();
+    }
+
     private boolean hasAnyFlag(int flagMask) {
         return hasAnyFlag(mState, flagMask);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index f713dca..f6bc785 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -18,6 +18,8 @@
 import android.graphics.Rect;
 import android.view.View;
 
+import androidx.annotation.CallSuper;
+
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 
@@ -33,11 +35,15 @@
     // Initialized in init.
     protected TaskbarControllers mControllers;
 
+    @CallSuper
     protected void init(TaskbarControllers taskbarControllers) {
         mControllers = taskbarControllers;
     }
 
-    protected void onDestroy() { }
+    @CallSuper
+    protected void onDestroy() {
+        mControllers = null;
+    }
 
     protected boolean isTaskbarTouchable() {
         return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index b21d677..19897a1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -66,6 +66,12 @@
         // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
         // DepthController to prevent optimizations which might occlude the layers behind
         mLauncher.getDepthController().setHasContentBehindLauncher(state.overviewUi);
+
+        if (isSplitSelectionState(state)) {
+            mRecentsView.applySplitPrimaryScrollOffset();
+        } else {
+            mRecentsView.resetSplitPrimaryScrollOffset();
+        }
     }
 
     @Override
@@ -90,9 +96,10 @@
         LauncherState currentState = mLauncher.getStateManager().getState();
         if (isSplitSelectionState(toState) && !isSplitSelectionState(currentState)) {
             builder.add(mRecentsView.createSplitSelectInitAnimation().buildAnim());
+        }
+        if (isSplitSelectionState(toState)) {
             mRecentsView.applySplitPrimaryScrollOffset();
-        } else if (!isSplitSelectionState(toState) && isSplitSelectionState(currentState)) {
-            builder.add(mRecentsView.cancelSplitSelect(true).buildAnim());
+        } else {
             mRecentsView.resetSplitPrimaryScrollOffset();
         }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index c554fd0..8f89d30 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -69,7 +69,7 @@
     }
 
     @Override
-    public boolean isTaskbarStashed() {
+    public boolean isTaskbarStashed(Launcher launcher) {
         return true;
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 4984b95..b733007 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -74,8 +74,7 @@
         return super.getVisibleElements(launcher)
                 & ~OVERVIEW_ACTIONS
                 & ~CLEAR_ALL_BUTTON
-                & ~VERTICAL_SWIPE_INDICATOR
-                | TASKBAR;
+                & ~VERTICAL_SWIPE_INDICATOR;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index a4eff87..08d0a80 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -92,7 +92,7 @@
     }
 
     @Override
-    public boolean isTaskbarStashed() {
+    public boolean isTaskbarStashed(Launcher launcher) {
         return true;
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
index d36e76b..969abc2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.util.Themes;
@@ -42,6 +43,10 @@
 
     @Override
     public int getWorkspaceScrimColor(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        if (dp.isTaskbarPresentInApps) {
+            return launcher.getColor(R.color.taskbar_background);
+        }
         return Themes.getAttrColor(launcher, R.attr.overviewScrimColor);
     }
 
@@ -53,6 +58,16 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return TASKBAR;
+        return NONE;
+    }
+
+    @Override
+    public boolean isTaskbarStashed(Launcher launcher) {
+        return !launcher.getDeviceProfile().isTaskbarPresentInApps;
+    }
+
+    @Override
+    public boolean isTaskbarAlignedWithHotseat(Launcher launcher) {
+        return false;
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
index 106375a..e79d56b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
@@ -17,8 +17,6 @@
 package com.android.launcher3.uioverrides.states;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -31,11 +29,6 @@
     }
 
     @Override
-    public void onBackPressed(Launcher launcher) {
-        launcher.getStateManager().goToState(OVERVIEW);
-    }
-
-    @Override
     public int getVisibleElements(Launcher launcher) {
         return SPLIT_PLACHOLDER_VIEW;
     }
@@ -43,12 +36,6 @@
     @Override
     public float getSplitSelectTranslation(Launcher launcher) {
         RecentsView recentsView = launcher.getOverviewPanel();
-        int splitPosition = recentsView.getSplitPlaceholder().getActiveSplitStagePosition();
-        if (!recentsView.shouldShiftThumbnailsForSplitSelect()) {
-            return 0f;
-        }
-        PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
-        int direction = orientationHandler.getSplitTranslationDirectionFactor(splitPosition);
-        return launcher.getResources().getDimension(R.dimen.split_placeholder_size) * direction;
+        return recentsView.getSplitSelectTranslation();
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index ff3c517..f6148a7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -236,8 +236,10 @@
         //   - RecentsView fade (if it's empty)
         PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
         xAnim.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1], LINEAR);
+        // Use QuickSwitchState instead of OverviewState to determine scrim color,
+        // since we need to take potential taskbar into account.
         xAnim.setViewBackgroundColor(mLauncher.getScrimView(),
-                toState.getWorkspaceScrimColor(mLauncher), LINEAR);
+                QUICK_SWITCH.getWorkspaceScrimColor(mLauncher), LINEAR);
         if (mRecentsView.getTaskViewCount() == 0) {
             xAnim.addFloat(mRecentsView, CONTENT_ALPHA, 0f, 1f, LINEAR);
         }
@@ -310,6 +312,11 @@
                 }
             });
             overviewAnim.start();
+
+            // Create an empty state transition so StateListeners get onStateTransitionStart().
+            mLauncher.getStateManager().createAnimationToNewWorkspace(
+                    OVERVIEW, config.duration, StateAnimationConfig.SKIP_ALL_ANIMATIONS)
+                    .dispatchOnStart();
             return;
         }
 
@@ -384,6 +391,7 @@
             config.animFlags = SKIP_ALL_ANIMATIONS;
             updateNonOverviewAnim(targetState, config);
             nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
+            mNonOverviewAnim.dispatchOnStart();
 
             new WorkspaceRevealAnim(mLauncher, false /* animateOverviewScrim */).start();
         } else {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index c9cbba1..0e5282a 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -55,6 +55,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
+import android.app.Activity;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
@@ -73,6 +74,7 @@
 import android.view.WindowInsets;
 import android.view.animation.Interpolator;
 import android.widget.Toast;
+import android.window.PictureInPictureSurfaceTransaction;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -89,6 +91,7 @@
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.tracing.InputConsumerProto;
 import com.android.launcher3.tracing.SwipeHandlerProto;
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface.AnimationFactory;
@@ -153,6 +156,17 @@
     protected MultiStateCallback mStateCallback;
     protected boolean mCanceled;
     private boolean mRecentsViewScrollLinked = false;
+    private final ActivityLifecycleCallbacksAdapter mLifecycleCallbacks =
+            new ActivityLifecycleCallbacksAdapter() {
+                @Override
+                public void onActivityDestroyed(Activity activity) {
+                    if (mActivity != activity) {
+                        return;
+                    }
+                    mRecentsView = null;
+                    mActivity = null;
+                }
+            };
 
     private static int getFlagForIndex(int index, String name) {
         if (DEBUG_STATES) {
@@ -416,6 +430,7 @@
         setupRecentsViewUi();
         linkRecentsViewScroll();
 
+        mActivity.registerActivityLifecycleCallbacks(mLifecycleCallbacks);
         return true;
     }
 
@@ -1247,7 +1262,8 @@
             HomeAnimationFactory homeAnimFactory =
                     createHomeAnimationFactory(cookies, duration, isTranslucent, appCanEnterPip,
                             runningTaskTarget);
-            mIsSwipingPipToHome = homeAnimFactory.supportSwipePipToHome() && appCanEnterPip;
+            mIsSwipingPipToHome = !mIsSwipeForStagedSplit
+                    && homeAnimFactory.supportSwipePipToHome() && appCanEnterPip;
             final RectFSpringAnim[] windowAnim;
             if (mIsSwipingPipToHome) {
                 mSwipePipToHomeAnimator = createWindowAnimationToPip(
@@ -1542,6 +1558,9 @@
 
     private void reset() {
         mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+        if (mActivity != null) {
+            mActivity.unregisterActivityLifecycleCallbacks(mLifecycleCallbacks);
+        }
     }
 
     /**
@@ -1711,7 +1730,7 @@
             // If there are no targets or the animation not started, then there is nothing to finish
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
         } else {
-            maybeFinishSwipePipToHome();
+            maybeFinishSwipeToHome();
             finishRecentsControllerToHome(
                     () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
         }
@@ -1720,10 +1739,11 @@
     }
 
     /**
-     * Resets the {@link #mIsSwipingPipToHome} and notifies SysUI that transition is finished
-     * if applicable. This should happen before {@link #finishRecentsControllerToHome(Runnable)}.
+     * Notifies SysUI that transition is finished if applicable and also pass leash transactions
+     * from Launcher to WM.
+     * This should happen before {@link #finishRecentsControllerToHome(Runnable)}.
      */
-    private void maybeFinishSwipePipToHome() {
+    private void maybeFinishSwipeToHome() {
         if (mIsSwipingPipToHome && mSwipePipToHomeAnimators[0] != null) {
             SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
                     mSwipePipToHomeAnimator.getComponentName(),
@@ -1734,6 +1754,18 @@
                     mSwipePipToHomeAnimator.getFinishTransaction(),
                     mSwipePipToHomeAnimator.getContentOverlay());
             mIsSwipingPipToHome = false;
+        } else if (mIsSwipeForStagedSplit) {
+            // Transaction to hide the task to avoid flicker for entering PiP from split-screen.
+            PictureInPictureSurfaceTransaction tx =
+                    new PictureInPictureSurfaceTransaction.Builder()
+                            .setAlpha(0f)
+                            .build();
+            int[] taskIds =
+                        LauncherSplitScreenListener.INSTANCE.getNoCreate().getRunningSplitTaskIds();
+            for (int taskId : taskIds) {
+                mRecentsAnimationController.setFinishTaskTransaction(taskId,
+                        tx, null /* overlay */);
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index a566765..1d4ed4c 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -221,6 +221,7 @@
 
             PointF taskDimension = getTaskDimension(context, dp);
             float scale = gridRect.height() / taskDimension.y;
+            scale = Math.min(scale, res.getFloat(R.dimen.overview_max_scale));
             int outWidth = Math.round(scale * taskDimension.x);
             int outHeight = Math.round(scale * taskDimension.y);
 
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 8a30aad..a72935b 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -115,8 +115,6 @@
     }
 
     private HomeAnimationFactory createIconHomeAnimationFactory(View workspaceView) {
-        final ResourceProvider rp = DynamicResource.provider(mActivity);
-        final float transY = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp));
         RectF iconLocation = new RectF();
         FloatingIconView floatingIconView = getFloatingIconView(mActivity, workspaceView,
                 true /* hideOriginal */, iconLocation, false /* isOpening */);
@@ -127,19 +125,15 @@
 
         return new FloatingViewHomeAnimationFactory(floatingIconView) {
 
-            // There is a delay in loading the icon, so we need to keep the window
-            // opaque until it is ready.
-            private boolean mIsFloatingIconReady = false;
-
             @Nullable
             @Override
             protected View getViewIgnoredInWorkspaceRevealAnimation() {
                 return workspaceView;
             }
 
+            @NonNull
             @Override
             public RectF getWindowTargetRect() {
-                super.getWindowTargetRect();
                 return iconLocation;
             }
 
@@ -152,15 +146,6 @@
             }
 
             @Override
-            public boolean keepWindowOpaque() {
-                if (mIsFloatingIconReady || floatingIconView.isVisibleToUser()) {
-                    mIsFloatingIconReady = true;
-                    return false;
-                }
-                return true;
-            }
-
-            @Override
             public void update(RectF currentRect, float progress, float radius) {
                 super.update(currentRect, progress, radius);
                 floatingIconView.update(1f /* alpha */, 255 /* fgAlpha */, currentRect, progress,
@@ -215,11 +200,6 @@
             }
 
             @Override
-            public boolean keepWindowOpaque() {
-                return false;
-            }
-
-            @Override
             public void update(RectF currentRect, float progress, float radius) {
                 super.update(currentRect, progress, radius);
                 final float fallbackBackgroundAlpha =
@@ -264,7 +244,7 @@
             }
         }
 
-        return mActivity.getWorkspace().getFirstMatchForAppClose(launchCookieItemId,
+        return mActivity.getFirstMatchForAppClose(launchCookieItemId,
                 runningTaskView.getTask().key.getComponent().getPackageName(),
                 UserHandle.of(runningTaskView.getTask().key.userId));
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index dd6392c..b502676 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -23,6 +23,7 @@
 import android.view.RemoteAnimationTarget;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.Utilities;
@@ -77,7 +78,7 @@
 
     public void notifyAnimationCanceled() {
         mCancelled = true;
-        onAnimationCanceled(null);
+        onAnimationCanceled(new HashMap<>());
     }
 
     // Called only in Q platform
@@ -167,16 +168,17 @@
          * Callback from the system when the recents animation is canceled. {@param thumbnailData}
          * is passed back for rendering screenshot to replace live tile.
          */
-        default void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {}
+        default void onRecentsAnimationCanceled(
+                @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {}
 
         /**
          * Callback made whenever the recents animation is finished.
          */
-        default void onRecentsAnimationFinished(RecentsAnimationController controller) {}
+        default void onRecentsAnimationFinished(@NonNull RecentsAnimationController controller) {}
 
         /**
          * Callback made when a task started from the recents is ready for an app transition.
          */
-        default void onTasksAppeared(RemoteAnimationTargetCompat[] appearedTaskTarget) {}
+        default void onTasksAppeared(@NonNull RemoteAnimationTargetCompat[] appearedTaskTarget) {}
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index b36cb0a..8e9b668 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -183,8 +183,6 @@
 
         public void setAnimation(RectFSpringAnim anim) { }
 
-        public boolean keepWindowOpaque() { return false; }
-
         public void update(RectF currentRect, float progress, float radius) { }
 
         public void onCancel() { }
@@ -338,9 +336,6 @@
             mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
             float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius);
             float alpha = mAnimationFactory.getWindowAlpha(progress);
-            if (mAnimationFactory.keepWindowOpaque()) {
-                alpha = 1f;
-            }
             mLocalTransformParams
                     .setTargetAlpha(alpha)
                     .setCornerRadius(cornerRadius);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 2543e6c..67e7f88 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -593,11 +593,11 @@
         }
     }
 
-    public void startShortcut(String packageName, String shortcutId, int stage, int position,
+    public void startShortcut(String packageName, String shortcutId, int position,
             Bundle options, UserHandle user) {
         if (mSplitScreen != null) {
             try {
-                mSplitScreen.startShortcut(packageName, shortcutId, stage, position, options,
+                mSplitScreen.startShortcut(packageName, shortcutId, position, options,
                         user);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startShortcut");
@@ -605,11 +605,11 @@
         }
     }
 
-    public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
+    public void startIntent(PendingIntent intent, Intent fillInIntent, int position,
             Bundle options) {
         if (mSplitScreen != null) {
             try {
-                mSplitScreen.startIntent(intent, fillInIntent, stage, position, options);
+                mSplitScreen.startIntent(intent, fillInIntent, position, options);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startIntent");
             }
@@ -682,7 +682,9 @@
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
         }
-        mRemoteTransitions.add(remoteTransition);
+        if (!mRemoteTransitions.contains(remoteTransition)) {
+            mRemoteTransitions.add(remoteTransition);
+        }
     }
 
     public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index e77ec78..97fc6d7 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -316,7 +316,7 @@
                 mt[i] = localMt;
 
                 Matrix localMti = new Matrix();
-                localMti.invert(localMt);
+                localMt.invert(localMti);
                 mti[i] = localMti;
             }
 
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 50b69dc..ff175f1 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -24,14 +24,22 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
+import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
 
+import android.util.FloatProperty;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
@@ -100,5 +108,31 @@
 
         setter.setViewBackgroundColor(mActivity.getScrimView(), state.getScrimColor(mActivity),
                 config.getInterpolator(ANIM_SCRIM_FADE, LINEAR));
+
+        RecentsState currentState = mActivity.getStateManager().getState();
+        if (isSplitSelectionState(state) && !isSplitSelectionState(currentState)) {
+            setter.add(mRecentsView.createSplitSelectInitAnimation().buildAnim());
+        }
+
+        Pair<FloatProperty, FloatProperty> taskViewsFloat =
+                mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset(
+                        TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
+                        mActivity.getDeviceProfile());
+        setter.setFloat(mRecentsView, taskViewsFloat.second, 0, LINEAR);
+        if (isSplitSelectionState(state)) {
+            mRecentsView.applySplitPrimaryScrollOffset();
+            setter.setFloat(mRecentsView, taskViewsFloat.first,
+                    mRecentsView.getSplitSelectTranslation(), LINEAR);
+        } else {
+            mRecentsView.resetSplitPrimaryScrollOffset();
+            setter.setFloat(mRecentsView, taskViewsFloat.first, 0, LINEAR);
+        }
+    }
+
+    /**
+     * @return true if {@param toState} is {@link RecentsState#OVERVIEW_SPLIT_SELECT}
+     */
+    private boolean isSplitSelectionState(@NonNull RecentsState toState) {
+        return toState == OVERVIEW_SPLIT_SELECT;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d2373c9..6d22b17 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -19,21 +19,23 @@
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
 import static com.android.quickstep.fallback.RecentsState.HOME;
 import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
+import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT;
 
 import android.animation.AnimatorSet;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.RecentsActivity;
@@ -73,6 +75,7 @@
     @Override
     public void startHome() {
         mActivity.startHome();
+        AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
     }
 
     /**
@@ -205,6 +208,13 @@
     }
 
     @Override
+    public void initiateSplitSelect(TaskView taskView,
+            @SplitConfigurationOptions.StagePosition int stagePosition) {
+        super.initiateSplitSelect(taskView, stagePosition);
+        mActivity.getStateManager().goToState(OVERVIEW_SPLIT_SELECT);
+    }
+
+    @Override
     public void onStateTransitionStart(RecentsState toState) {
         setOverviewStateEnabled(true);
         setOverviewGridEnabled(toState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
@@ -243,12 +253,4 @@
         // Do not let touch escape to siblings below this view.
         return result || mActivity.getStateManager().getState().overviewUi();
     }
-
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-
-        // Reset modal state if full configuration changes
-        setModalStateEnabled(false);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index 917b58a..15feb18 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -51,6 +51,8 @@
             FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN | FLAG_OVERVIEW_UI);
     public static final RecentsState HOME = new RecentsState(3, 0);
     public static final RecentsState BG_LAUNCHER = new LauncherState(4, 0);
+    public static final RecentsState OVERVIEW_SPLIT_SELECT = new RecentsState(5,
+            FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_OVERVIEW_UI);
 
     public final int ordinal;
     private final int mFlags;
diff --git a/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java b/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java
new file mode 100644
index 0000000..861ff96
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.annotation.CallSuper;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator;
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Animation that moves launcher icons and widgets from center to the sides (final position)
+ */
+public abstract class BaseUnfoldMoveFromCenterAnimator implements TransitionProgressListener {
+
+    private final UnfoldMoveFromCenterAnimator mMoveFromCenterAnimation;
+
+    private final Map<ViewGroup, Boolean> mOriginalClipToPadding = new HashMap<>();
+    private final Map<ViewGroup, Boolean> mOriginalClipChildren = new HashMap<>();
+
+    public BaseUnfoldMoveFromCenterAnimator(WindowManager windowManager) {
+        mMoveFromCenterAnimation = new UnfoldMoveFromCenterAnimator(windowManager,
+                new LauncherViewsMoveFromCenterTranslationApplier());
+    }
+
+    @CallSuper
+    @Override
+    public void onTransitionStarted() {
+        mMoveFromCenterAnimation.updateDisplayProperties();
+        onPrepareViewsForAnimation();
+        onTransitionProgress(0f);
+    }
+
+    @CallSuper
+    @Override
+    public void onTransitionProgress(float progress) {
+        mMoveFromCenterAnimation.onTransitionProgress(progress);
+    }
+
+    @CallSuper
+    @Override
+    public void onTransitionFinished() {
+        mMoveFromCenterAnimation.onTransitionFinished();
+        mMoveFromCenterAnimation.clearRegisteredViews();
+
+        mOriginalClipChildren.clear();
+        mOriginalClipToPadding.clear();
+    }
+
+    protected void onPrepareViewsForAnimation() {
+
+    }
+
+    protected void registerViewForAnimation(View view) {
+        mMoveFromCenterAnimation.registerViewForAnimation(view);
+    }
+
+    protected void disableClipping(ViewGroup view) {
+        mOriginalClipToPadding.put(view, view.getClipToPadding());
+        mOriginalClipChildren.put(view, view.getClipChildren());
+        view.setClipToPadding(false);
+        view.setClipChildren(false);
+    }
+
+    protected void restoreClipping(ViewGroup view) {
+        final Boolean originalClipToPadding = mOriginalClipToPadding.get(view);
+        if (originalClipToPadding != null) {
+            view.setClipToPadding(originalClipToPadding);
+        }
+        final Boolean originalClipChildren = mOriginalClipChildren.get(view);
+        if (originalClipChildren != null) {
+            view.setClipChildren(originalClipChildren);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
index b39412b..6b6bd6a 100644
--- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
+++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
@@ -18,14 +18,17 @@
 import static com.android.launcher3.Utilities.comp;
 
 import android.annotation.Nullable;
-import android.view.ViewTreeObserver;
 import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import androidx.core.view.OneShotPreDrawListener;
 
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.util.HorizontalInsettableView;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
+import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider;
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
 
 /**
@@ -43,6 +46,7 @@
     private HorizontalInsettableView mQsbInsettable;
 
     private final ScopedUnfoldTransitionProgressProvider mProgressProvider;
+    private final NaturalRotationUnfoldProgressProvider mNaturalOrientationProgressProvider;
 
     public LauncherUnfoldAnimationController(
             Launcher launcher,
@@ -51,10 +55,19 @@
         mLauncher = launcher;
         mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
                 unfoldTransitionProgressProvider);
+        mNaturalOrientationProgressProvider = new NaturalRotationUnfoldProgressProvider(launcher,
+                WindowManagerGlobal.getWindowManagerService(), mProgressProvider);
+        mNaturalOrientationProgressProvider.init();
 
+        // Animated in all orientations
         mProgressProvider.addCallback(new UnfoldMoveFromCenterWorkspaceAnimator(launcher,
                 windowManager));
-        mProgressProvider.addCallback(new QsbAnimationListener());
+
+        // Animated only in natural orientation
+        mNaturalOrientationProgressProvider
+                .addCallback(new QsbAnimationListener());
+        mNaturalOrientationProgressProvider
+                .addCallback(new UnfoldMoveFromCenterHotseatAnimator(launcher, windowManager));
     }
 
     /**
@@ -66,17 +79,8 @@
             mQsbInsettable = (HorizontalInsettableView) hotseat.getQsb();
         }
 
-        final ViewTreeObserver obs = mLauncher.getWorkspace().getViewTreeObserver();
-        obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-            @Override
-            public boolean onPreDraw() {
-                if (obs.isAlive()) {
-                    mProgressProvider.setReadyToHandleTransition(true);
-                    obs.removeOnPreDrawListener(this);
-                }
-                return true;
-            }
-        });
+        OneShotPreDrawListener.add(mLauncher.getWorkspace(),
+                () -> mProgressProvider.setReadyToHandleTransition(true));
     }
 
     /**
@@ -92,6 +96,7 @@
      */
     public void onDestroy() {
         mProgressProvider.destroy();
+        mNaturalOrientationProgressProvider.destroy();
     }
 
     private class QsbAnimationListener implements TransitionProgressListener {
diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java
new file mode 100644
index 0000000..dc97dd6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+
+/**
+ * Animation that moves hotseat icons from center to the sides (final position)
+ */
+public class UnfoldMoveFromCenterHotseatAnimator extends BaseUnfoldMoveFromCenterAnimator {
+
+    private final Launcher mLauncher;
+
+    public UnfoldMoveFromCenterHotseatAnimator(Launcher launcher, WindowManager windowManager) {
+        super(windowManager);
+        mLauncher = launcher;
+    }
+
+    @Override
+    protected void onPrepareViewsForAnimation() {
+        Hotseat hotseat = mLauncher.getHotseat();
+
+        ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets();
+        disableClipping(hotseat);
+
+        for (int i = 0; i < hotseatIcons.getChildCount(); i++) {
+            View child = hotseatIcons.getChildAt(i);
+            registerViewForAnimation(child);
+        }
+    }
+
+    @Override
+    public void onTransitionFinished() {
+        restoreClipping(mLauncher.getHotseat());
+        super.onTransitionFinished();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
index 95403b2..3d72398 100644
--- a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
@@ -16,44 +16,28 @@
 package com.android.quickstep.util;
 
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.WindowManager;
 
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
-import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator;
-import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
-
-import java.util.HashMap;
-import java.util.Map;
 
 /**
  * Animation that moves launcher icons and widgets from center to the sides (final position)
  */
-public class UnfoldMoveFromCenterWorkspaceAnimator
-        implements UnfoldTransitionProgressProvider.TransitionProgressListener {
+public class UnfoldMoveFromCenterWorkspaceAnimator extends BaseUnfoldMoveFromCenterAnimator {
 
     private final Launcher mLauncher;
-    private final UnfoldMoveFromCenterAnimator mMoveFromCenterAnimation;
-
-    private final Map<ViewGroup, Boolean> mOriginalClipToPadding = new HashMap<>();
-    private final Map<ViewGroup, Boolean> mOriginalClipChildren = new HashMap<>();
 
     public UnfoldMoveFromCenterWorkspaceAnimator(Launcher launcher, WindowManager windowManager) {
+        super(windowManager);
         mLauncher = launcher;
-        mMoveFromCenterAnimation = new UnfoldMoveFromCenterAnimator(windowManager,
-                new LauncherViewsMoveFromCenterTranslationApplier());
     }
 
     @Override
-    public void onTransitionStarted() {
-        mMoveFromCenterAnimation.updateDisplayProperties();
-
+    protected void onPrepareViewsForAnimation() {
         Workspace workspace = mLauncher.getWorkspace();
-        Hotseat hotseat = mLauncher.getHotseat();
 
         // App icons and widgets
         workspace
@@ -65,57 +49,17 @@
 
                     for (int i = 0; i < itemsContainer.getChildCount(); i++) {
                         View child = itemsContainer.getChildAt(i);
-                        mMoveFromCenterAnimation.registerViewForAnimation(child);
+                        registerViewForAnimation(child);
                     }
                 });
 
         disableClipping(workspace);
-
-        // Hotseat icons
-        ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets();
-        disableClipping(hotseat);
-
-        for (int i = 0; i < hotseatIcons.getChildCount(); i++) {
-            View child = hotseatIcons.getChildAt(i);
-            mMoveFromCenterAnimation.registerViewForAnimation(child);
-        }
-
-        onTransitionProgress(0f);
-    }
-
-    @Override
-    public void onTransitionProgress(float progress) {
-        mMoveFromCenterAnimation.onTransitionProgress(progress);
     }
 
     @Override
     public void onTransitionFinished() {
-        mMoveFromCenterAnimation.onTransitionFinished();
-        mMoveFromCenterAnimation.clearRegisteredViews();
-
         restoreClipping(mLauncher.getWorkspace());
         mLauncher.getWorkspace().forEachVisiblePage(page -> restoreClipping((CellLayout) page));
-        restoreClipping(mLauncher.getHotseat());
-
-        mOriginalClipChildren.clear();
-        mOriginalClipToPadding.clear();
-    }
-
-    private void disableClipping(ViewGroup view) {
-        mOriginalClipToPadding.put(view, view.getClipToPadding());
-        mOriginalClipChildren.put(view, view.getClipChildren());
-        view.setClipToPadding(false);
-        view.setClipChildren(false);
-    }
-
-    private void restoreClipping(ViewGroup view) {
-        final Boolean originalClipToPadding = mOriginalClipToPadding.get(view);
-        if (originalClipToPadding != null) {
-            view.setClipToPadding(originalClipToPadding);
-        }
-        final Boolean originalClipChildren = mOriginalClipChildren.get(view);
-        if (originalClipChildren != null) {
-            view.setClipChildren(originalClipChildren);
-        }
+        super.onTransitionFinished();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index 325ec04..18ab3bb 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -17,8 +17,9 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -43,7 +44,7 @@
 
     private SplitPlaceholderView mSplitPlaceholderView;
     private RectF mStartingPosition;
-    private final Launcher mLauncher;
+    private final BaseDraggingActivity mActivity;
     private final boolean mIsRtl;
     private final Rect mOutline = new Rect();
     private PagedOrientationHandler mOrientationHandler;
@@ -59,7 +60,7 @@
 
     public FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
+        mActivity = BaseActivity.fromContext(context);
         mIsRtl = Utilities.isRtl(getResources());
     }
 
@@ -114,7 +115,7 @@
     public void updateInitialPositionForView(TaskView originalView) {
         View thumbnail = originalView.getThumbnail();
         Rect viewBounds = new Rect(0, 0, thumbnail.getWidth(), thumbnail.getHeight());
-        Utilities.getBoundsForViewInDragLayer(mLauncher.getDragLayer(), thumbnail, viewBounds,
+        Utilities.getBoundsForViewInDragLayer(mActivity.getDragLayer(), thumbnail, viewBounds,
                 true /* ignoreTransform */, null /* recycle */,
                 mStartingPosition);
         mStartingPosition.offset(originalView.getTranslationX(), originalView.getTranslationY());
@@ -129,9 +130,7 @@
     public void update(RectF position, float progress, float windowRadius) {
         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
 
-        float dX = mIsRtl
-                ? position.left - (lp.getMarginStart() - lp.width)
-                : position.left - lp.getMarginStart();
+        float dX = position.left - mStartingPosition.left;
         float dY = position.top - lp.topMargin;
 
         setTranslationX(dX);
@@ -152,27 +151,31 @@
         mOrientationHandler.setSecondaryScale(mSplitPlaceholderView.getIconView(), childScaleY);
     }
 
+    public void updateOrientationHandler(PagedOrientationHandler orientationHandler) {
+        mOrientationHandler = orientationHandler;
+        mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
+    }
+
     protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
         mStartingPosition.set(pos);
         lp.ignoreInsets = true;
         // Position the floating view exactly on top of the original
         lp.topMargin = Math.round(pos.top);
         if (mIsRtl) {
-            lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - pos.right));
+            lp.setMarginStart(mActivity.getDeviceProfile().widthPx - Math.round(pos.right));
         } else {
             lp.setMarginStart(Math.round(pos.left));
         }
+
         // Set the properties here already to make sure they are available when running the first
         // animation frame.
-        int left = mIsRtl
-                ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
-                : lp.leftMargin;
+        int left = (int) pos.left;
         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
     }
 
     public void addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds,
             View viewToCover, boolean fadeWithThumbnail) {
-        final BaseDragLayer dragLayer = mLauncher.getDragLayer();
+        final BaseDragLayer dragLayer = mActivity.getDragLayer();
         int[] dragLayerBounds = new int[2];
         dragLayer.getLocationOnScreen(dragLayerBounds);
         SplitOverlayProperties prop = new SplitOverlayProperties(endBounds,
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 00f541d..b215ef1 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -157,10 +157,20 @@
     @Nullable
     @Override
     public RunnableList launchTaskAnimated() {
-        getRecentsView().getSplitPlaceholder().launchTasks(this /*groupedTaskView*/,
-                null /*callback*/,
+        if (mTask == null || mSecondaryTask == null) {
+            return null;
+        }
+
+        RunnableList endCallback = new RunnableList();
+        RecentsView recentsView = getRecentsView();
+        // Callbacks run from remote animation when recents animation not currently running
+        recentsView.getSplitPlaceholder().launchTasks(this /*groupedTaskView*/,
+                success -> endCallback.executeAllAndDestroy(),
                 false /* freezeTaskList */);
-        return null;
+
+        // Callbacks get run from recentsView for case when recents animation already running
+        recentsView.addSideTaskLaunchCallback(endCallback);
+        return endCallback;
     }
 
     @Override
@@ -239,7 +249,7 @@
         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
 
         getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2,
-                taskIconHeight, mSnapshotView.getWidth(), mSnapshotView.getHeight(),
+                taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(),
                 isRtl, deviceProfile, mSplitBoundsConfig);
     }
 
@@ -248,4 +258,10 @@
         super.updateSnapshotRadius();
         mSnapshotView2.setFullscreenParams(mCurrentFullscreenParams);
     }
+
+    @Override
+    protected void setIconAndDimTransitionProgress(float progress, boolean invert) {
+        super.setIconAndDimTransitionProgress(progress, invert);
+        mIconView2.setAlpha(mIconView.getAlpha());
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index d001690..e0395ea 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -24,7 +24,6 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
@@ -32,6 +31,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.statehandlers.DepthController;
@@ -70,6 +70,7 @@
     @Override
     public void startHome() {
         mActivity.getStateManager().goToState(NORMAL);
+        AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
     }
 
     @Override
@@ -167,12 +168,4 @@
         super.initiateSplitSelect(taskView, stagePosition);
         mActivity.getStateManager().goToState(LauncherState.OVERVIEW_SPLIT_SELECT);
     }
-
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        // If overview is in modal state when rotate, reset it to overview state without running
-        // animation.
-        setModalStateEnabled(false);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index b6bf59f..0294828 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -22,7 +22,10 @@
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
@@ -80,7 +83,7 @@
     private static final int INDEX_HIDDEN_FLAGS_ALPHA = 3;
 
     private final MultiValueAlpha mMultiValueAlpha;
-    private View mSplitButton;
+    private Button mSplitButton;
 
     @ActionsHiddenFlags
     private int mHiddenFlags;
@@ -214,7 +217,18 @@
     public void setDp(DeviceProfile dp) {
         mDp = dp;
         updateVerticalMargin(SysUINavigationMode.getMode(getContext()));
+
+        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+                dp.isVerticalBarLayout() ? 0 : dp.overviewActionsButtonSpacing,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+        params.weight = dp.isVerticalBarLayout() ? 1 : 0;
+        findViewById(R.id.action_split_space).setLayoutParams(params);
+
         requestLayout();
+
+        mSplitButton.setCompoundDrawablesWithIntrinsicBounds(
+                (dp.isLandscape ? R.drawable.ic_split_horizontal : R.drawable.ic_split_vertical),
+                0, 0, 0);
     }
 
     public void setSplitButtonVisible(boolean visible) {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index e9010d2..c5b191e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -91,6 +91,7 @@
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseBooleanArray;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -107,6 +108,7 @@
 import android.widget.OverScroller;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.core.graphics.ColorUtils;
@@ -181,6 +183,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 /**
@@ -627,7 +630,7 @@
      * this doesn't get adjusted to reflect the new child count after the taskView is dismissed/
      * removed from recentsView
      */
-    private int mSplitHiddenTaskViewIndex;
+    private int mSplitHiddenTaskViewIndex = -1;
     @Nullable
     private FloatingTaskView mFirstFloatingTaskView;
     @Nullable
@@ -822,7 +825,7 @@
     @Override
     public void onTaskIconChanged(String pkg, UserHandle user) {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView tv = getTaskViewAt(i);
+            TaskView tv = requireTaskViewAt(i);
             Task task = tv.getTask();
             if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
                     && task.key.userId == user.getIdentifier()) {
@@ -1039,6 +1042,17 @@
         }
     }
 
+    public boolean isTaskViewFullyVisible(TaskView tv) {
+        if (showAsGrid()) {
+            int screenStart = mOrientationHandler.getPrimaryScroll(this);
+            int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
+            return isTaskViewFullyWithinBounds(tv, screenStart, screenEnd);
+        } else {
+            // For now, just check if it's the active task
+            return indexOfChild(tv) == getNextPage();
+        }
+    }
+
     @Nullable
     private TaskView getLastGridTaskView() {
         return getLastGridTaskView(getTopRowIdArray(), getBottomRowIdArray());
@@ -1085,6 +1099,15 @@
                 && taskEnd <= end);
     }
 
+    private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
+        int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
+                showAsFullscreen(), showAsGrid());
+        int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
+                showAsFullscreen()));
+        int taskEnd = taskStart + taskSize;
+        return taskStart >= start && taskEnd <= end;
+    }
+
     /**
      * Returns true if the task is in expected scroll position.
      *
@@ -1104,7 +1127,7 @@
         }
 
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             int[] taskIds = taskView.getTaskIds();
             if (taskIds[0] == taskId || taskIds[1] == taskId) {
                 return taskView;
@@ -1121,9 +1144,6 @@
             // Reset the running task when leaving overview since it can still have a reference to
             // its thumbnail
             mTmpRunningTasks = null;
-            if (mSplitSelectStateController.isSplitSelectActive()) {
-                cancelSplitSelect(false);
-            }
             // Remove grouped tasks and recycle once we exit overview
             int taskCount = getTaskViewCount();
             for (int i = 0; i < taskCount; i++) {
@@ -1181,7 +1201,7 @@
         if (showAsGrid()) {
             int taskCount = getTaskViewCount();
             for (int i = 0; i < taskCount; i++) {
-                TaskView taskView = getTaskViewAt(i);
+                TaskView taskView = requireTaskViewAt(i);
                 if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
                     // Keep consuming events to pass to delegate
                     return true;
@@ -1244,7 +1264,7 @@
     @Override
     protected void onNotSnappingToPageInFreeScroll() {
         int finalPos = mScroller.getFinalX();
-        if (!showAsGrid() && finalPos > mMinScroll && finalPos < mMaxScroll) {
+        if (finalPos > mMinScroll && finalPos < mMaxScroll) {
             int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
             int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
 
@@ -1258,6 +1278,19 @@
                             ? mMaxScroll
                             : getScrollForPage(mNextPage);
 
+            if (showAsGrid()) {
+                if (isSplitSelectionActive()) {
+                    return;
+                }
+                TaskView taskView = getTaskViewAt(mNextPage);
+                // Only snap to fully visible focused task.
+                if (taskView == null
+                        || !taskView.isFocusedTask()
+                        || !isTaskViewFullyVisible(taskView)) {
+                    return;
+                }
+            }
+
             mScroller.setFinalX(pageSnapped);
             // Ensure the scroll/snap doesn't happen too fast;
             int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
@@ -1418,7 +1451,7 @@
             if (runningTaskId != -1) {
                 targetPage = indexOfChild(newRunningTaskView);
             } else if (getTaskViewCount() > 0) {
-                targetPage = indexOfChild(getTaskViewAt(0));
+                targetPage = indexOfChild(requireTaskViewAt(0));
             }
         } else if (currentTaskId != -1) {
             currentTaskView = getTaskViewByTaskId(currentTaskId);
@@ -1452,7 +1485,7 @@
 
     private void removeTasksViewsAndClearAllButton() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            removeView(getTaskViewAt(i));
+            removeView(requireTaskViewAt(i));
         }
         if (indexOfChild(mClearAllButton) != -1) {
             removeView(mClearAllButton);
@@ -1498,7 +1531,7 @@
 
     public void resetTaskVisuals() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             if (mIgnoreResetTaskId != taskView.getTaskIds()[0]) {
                 taskView.resetViewTransforms();
                 taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
@@ -1508,6 +1541,16 @@
             }
         }
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            // resetTaskVisuals is called at the end of dismiss animation which could update
+            // primary and secondary translation of the live tile cut out. We will need to do so
+            // here accordingly.
+            runActionOnRemoteHandles(remoteTargetHandle -> {
+                TaskViewSimulator simulator = remoteTargetHandle.getTaskViewSimulator();
+                simulator.taskPrimaryTranslation.value = 0;
+                simulator.taskSecondaryTranslation.value = 0;
+                simulator.fullScreenProgress.value = 0;
+                simulator.recentsViewScale.value = 1;
+            });
             // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
             // null.
             if (!mRunningTaskShowScreenshot) {
@@ -1529,7 +1572,7 @@
         mFullscreenProgress = fullscreenProgress;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
+            requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
         }
         mClearAllButton.setFullscreenProgress(fullscreenProgress);
 
@@ -1591,7 +1634,7 @@
                 || !mOrientationHandler.equals(oldOrientationHandler)) {
             // Changed orientations, update controllers so they intercept accordingly.
             mActivity.getDragLayer().recreateControllers();
-            setModalStateEnabled(false);
+            onOrientationChanged();
         }
 
         boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0
@@ -1610,6 +1653,15 @@
         setCurrentPage(mCurrentPage);
     }
 
+    private void onOrientationChanged() {
+        // If overview is in modal state when rotate, reset it to overview state without running
+        // animation.
+        setModalStateEnabled(false);
+        if (isSplitSelectionActive()) {
+            onRotateInSplitSelectionState();
+        }
+    }
+
     // Update task size and padding that are dependent on DeviceProfile and insets.
     private void updateSizeAndPadding() {
         DeviceProfile dp = mActivity.getDeviceProfile();
@@ -1656,7 +1708,7 @@
 
         float accumulatedTranslationX = 0;
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             taskView.updateTaskSize();
             taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX);
             taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f);
@@ -1802,7 +1854,7 @@
 
         // Update the task data for the in/visible children
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             Task task = taskView.getTask();
             int index = indexOfChild(taskView);
             boolean visible;
@@ -1945,7 +1997,7 @@
         }
 
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             if (taskView.getTaskViewId() == taskViewId) {
                 return taskView;
             }
@@ -2043,7 +2095,7 @@
         int runningIndex = getCurrentPage();
         AnimatorSet as = new AnimatorSet();
         for (int i = 0; i < getTaskViewCount(); i++) {
-            View taskView = getTaskViewAt(i);
+            View taskView = requireTaskViewAt(i);
             if (runningIndex == i && taskView.getAlpha() != 0) {
                 continue;
             }
@@ -2054,7 +2106,7 @@
 
     private void updateChildTaskOrientations() {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).setOrientationState(mOrientationState);
+            requireTaskViewAt(i).setOrientationState(mOrientationState);
         }
         TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU);
         if (tv != null) {
@@ -2259,7 +2311,7 @@
             mTaskIconScaledDown = isScaledDown;
             int taskCount = getTaskViewCount();
             for (int i = 0; i < taskCount; i++) {
-                getTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+                requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
             }
         }
     }
@@ -2275,7 +2327,7 @@
         mTaskIconScaledDown = false;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             taskView.setIconScaleAnimStartProgress(0f);
             taskView.animateIconScaleAndDimIntoView();
         }
@@ -2347,7 +2399,7 @@
             mTopRowIdSet.clear();
         }
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing;
             // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
             // which case keep tasks in their respective rows. For the running task, don't join
@@ -2412,7 +2464,7 @@
                         if (j == focusedTaskIndex) {
                             continue;
                         }
-                        widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+                        widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
                     }
 
                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
@@ -2431,7 +2483,7 @@
                         if (j == focusedTaskIndex) {
                             continue;
                         }
-                        widthOffset += getTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+                        widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
                     }
 
                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
@@ -2509,7 +2561,7 @@
         }
 
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX
                     + snappedTaskNonGridScrollAdjustment);
         }
@@ -2550,7 +2602,7 @@
         mGridProgress = gridProgress;
 
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setGridProgress(gridProgress);
+            requireTaskViewAt(i).setGridProgress(gridProgress);
         }
         mClearAllButton.setGridProgress(gridProgress);
     }
@@ -2713,7 +2765,7 @@
                         mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
                 // Pick the next focused task from the preferred row.
                 for (int i = 0; i < taskCount; i++) {
-                    TaskView taskView = getTaskViewAt(i);
+                    TaskView taskView = requireTaskViewAt(i);
                     if (taskView == dismissedTaskView) {
                         continue;
                     }
@@ -2816,7 +2868,7 @@
                                 + (taskCount - 1) * halfAdditionalDismissTranslationOffset,
                         END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1);
                 for (int i = 0; i < taskCount; i++) {
-                    TaskView taskView = getTaskViewAt(i);
+                    TaskView taskView = requireTaskViewAt(i);
                     anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff,
                             clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1));
                     dismissTranslationInterpolationEnd = Utilities.boundToRange(
@@ -3132,7 +3184,7 @@
                             // Rebalance tasks in the grid
                             int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
                             if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
-                                TaskView taskView = getTaskViewAt(highestVisibleTaskIndex);
+                                TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex);
 
                                 boolean shouldRebalance;
                                 int screenStart = mOrientationHandler.getPrimaryScroll(
@@ -3230,7 +3282,7 @@
         IntArray topArray = new IntArray(mTopRowIdSet.size());
         int taskViewCount = getTaskViewCount();
         for (int i = 0; i < taskViewCount; i++) {
-            int taskViewId = getTaskViewAt(i).getTaskViewId();
+            int taskViewId = requireTaskViewAt(i).getTaskViewId();
             if (mTopRowIdSet.contains(taskViewId)) {
                 topArray.add(taskViewId);
             }
@@ -3249,7 +3301,7 @@
         IntArray bottomArray = new IntArray(bottomRowIdArraySize);
         int taskViewCount = getTaskViewCount();
         for (int i = 0; i < taskViewCount; i++) {
-            int taskViewId = getTaskViewAt(i).getTaskViewId();
+            int taskViewId = requireTaskViewAt(i).getTaskViewId();
             if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
                 bottomArray.add(taskViewId);
             }
@@ -3304,7 +3356,7 @@
      * {@link #mFirstFloatingTaskView}.
      */
     public boolean shouldShiftThumbnailsForSplitSelect() {
-        return !mActivity.getDeviceProfile().isTablet;
+        return !mActivity.getDeviceProfile().isTablet || !mActivity.getDeviceProfile().isLandscape;
     }
 
     protected void onDismissAnimationEnds() {
@@ -3319,7 +3371,7 @@
 
         int count = getTaskViewCount();
         for (int i = 0; i < count; i++) {
-            addDismissedTaskAnimations(getTaskViewAt(i), duration, anim);
+            addDismissedTaskAnimations(requireTaskViewAt(i), duration, anim);
         }
 
         mPendingAnimation = anim;
@@ -3442,7 +3494,7 @@
         mContentAlpha = alpha;
         int runningTaskId = getTaskIdsForRunningTaskView()[0];
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            TaskView child = getTaskViewAt(i);
+            TaskView child = requireTaskViewAt(i);
             int[] childTaskIds = child.getTaskIds();
             if (!mRunningTaskTileHidden ||
                     (childTaskIds[0] != runningTaskId && childTaskIds[1] != runningTaskId)) {
@@ -3490,6 +3542,7 @@
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         updateRecentsRotation();
+        onOrientationChanged();
     }
 
     /**
@@ -3543,6 +3596,14 @@
         return child instanceof TaskView ? (TaskView) child : null;
     }
 
+    /**
+     * A version of {@link #getTaskViewAt} when the caller is sure about the input index.
+     */
+    @NonNull
+    private TaskView requireTaskViewAt(int index) {
+        return Objects.requireNonNull(getTaskViewAt(index));
+    }
+
     public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
         mOnEmptyMessageUpdatedListener = listener;
     }
@@ -3759,7 +3820,7 @@
     protected void setTaskViewsResistanceTranslation(float translation) {
         mTaskViewsSecondaryTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView task = getTaskViewAt(i);
+            TaskView task = requireTaskViewAt(i);
             task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
         }
         runActionOnRemoteHandles(
@@ -3769,14 +3830,14 @@
 
     private void updateTaskViewsSnapshotRadius() {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).updateSnapshotRadius();
+            requireTaskViewAt(i).updateSnapshotRadius();
         }
     }
 
     protected void setTaskViewsPrimarySplitTranslation(float translation) {
         mTaskViewsPrimarySplitTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView task = getTaskViewAt(i);
+            TaskView task = requireTaskViewAt(i);
             task.getPrimarySplitTranslationProperty().set(task, translation);
         }
     }
@@ -3784,7 +3845,7 @@
     protected void setTaskViewsSecondarySplitTranslation(float translation) {
         mTaskViewsSecondarySplitTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             if (taskView == mSplitHiddenTaskView) {
                 continue;
             }
@@ -3796,13 +3857,18 @@
      * Apply scroll offset to children of RecentsView when entering split select.
      */
     public void applySplitPrimaryScrollOffset() {
+        float taskSplitScrollOffsetPrimary = 0f;
+        float clearAllSplitScrollOffsetPrimar = 0f;
         if (isSplitPlaceholderFirstInGrid()) {
-            for (int i = 0; i < getTaskViewCount(); i++) {
-                getTaskViewAt(i).setSplitScrollOffsetPrimary(mSplitPlaceholderSize);
-            }
+            taskSplitScrollOffsetPrimary = mSplitPlaceholderSize;
         } else if (isSplitPlaceholderLastInGrid()) {
-            mClearAllButton.setSplitSelectScrollOffsetPrimary(-mSplitPlaceholderSize);
+            clearAllSplitScrollOffsetPrimar = -mSplitPlaceholderSize;
         }
+
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            requireTaskViewAt(i).setSplitScrollOffsetPrimary(taskSplitScrollOffsetPrimary);
+        }
+        mClearAllButton.setSplitSelectScrollOffsetPrimary(clearAllSplitScrollOffsetPrimar);
     }
 
     /**
@@ -3840,7 +3906,7 @@
      */
     public void resetSplitPrimaryScrollOffset() {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).setSplitScrollOffsetPrimary(0);
+            requireTaskViewAt(i).setSplitScrollOffsetPrimary(0);
         }
         mClearAllButton.setSplitSelectScrollOffsetPrimary(0);
     }
@@ -3920,111 +3986,11 @@
         pendingAnimation.buildAnim().start();
     }
 
-    public PendingAnimation cancelSplitSelect(boolean animate) {
-        SplitSelectStateController splitController = mSplitSelectStateController;
-        @StagePosition int stagePosition = splitController.getActiveSplitStagePosition();
-        Rect initialBounds = splitController.getInitialBounds();
-        splitController.resetState();
-        int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext());
-        PendingAnimation pendingAnim = new PendingAnimation(duration);
-        mSplitToast.cancel();
-        mSplitUnsupportedToast.cancel();
-        if (!animate) {
-            resetFromSplitSelectionState();
-            return pendingAnim;
-        }
-
-        addViewInLayout(mSplitHiddenTaskView, mSplitHiddenTaskViewIndex,
-                mSplitHiddenTaskView.getLayoutParams());
-        mSplitHiddenTaskView.setAlpha(0);
-        int[] oldScroll = new int[getChildCount()];
-        getPageScrolls(oldScroll, false,
-                view -> view.getVisibility() != GONE && view != mSplitHiddenTaskView);
-
-        int[] newScroll = new int[getChildCount()];
-        getPageScrolls(newScroll, false, SIMPLE_SCROLL_LOGIC);
-
-        boolean needsCurveUpdates = false;
-        for (int i = mSplitHiddenTaskViewIndex; i >= 0; i--) {
-            View child = getChildAt(i);
-            if (child == mSplitHiddenTaskView) {
-                TaskView taskView = (TaskView) child;
-
-                int dir = mOrientationHandler.getSplitTaskViewDismissDirection(stagePosition,
-                        mActivity.getDeviceProfile());
-                FloatProperty<TaskView> dismissingTaskViewTranslate;
-                Rect hiddenBounds = new Rect(taskView.getLeft(), taskView.getTop(),
-                        taskView.getRight(), taskView.getBottom());
-                int distanceDelta = 0;
-                if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_SECONDARY_NEGATIVE) {
-                    dismissingTaskViewTranslate = taskView
-                            .getSecondaryDissmissTranslationProperty();
-                    distanceDelta = initialBounds.top - hiddenBounds.top;
-                    taskView.layout(initialBounds.left, hiddenBounds.top, initialBounds.right,
-                            hiddenBounds.bottom);
-                } else {
-                    dismissingTaskViewTranslate = taskView
-                            .getPrimaryDismissTranslationProperty();
-                    distanceDelta = initialBounds.left - hiddenBounds.left;
-                    taskView.layout(hiddenBounds.left, initialBounds.top, hiddenBounds.right,
-                            initialBounds.bottom);
-                    if (dir == PagedOrientationHandler.SPLIT_TRANSLATE_PRIMARY_POSITIVE) {
-                        distanceDelta *= -1;
-                    }
-                }
-                pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView,
-                        dismissingTaskViewTranslate,
-                        distanceDelta));
-                pendingAnim.add(ObjectAnimator.ofFloat(mSplitHiddenTaskView, ALPHA, 1));
-            } else {
-                // If insertion is on last index (furthest from clear all), we directly add the view
-                // else we translate all views to the right of insertion index further right,
-                // ignore views to left
-                if (showAsGrid()) {
-                    // TODO(b/186800707) handle more elegantly for grid
-                    continue;
-                }
-                int scrollDiff = newScroll[i] - oldScroll[i];
-                if (scrollDiff != 0) {
-                    FloatProperty translationProperty = child instanceof TaskView
-                            ? ((TaskView) child).getPrimaryDismissTranslationProperty()
-                            : mOrientationHandler.getPrimaryViewTranslate();
-
-                    ResourceProvider rp = DynamicResource.provider(mActivity);
-                    SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
-                            .setDampingRatio(
-                                    rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
-                            .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
-                    pendingAnim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
-                            .setDuration(duration), ACCEL, sp);
-                    needsCurveUpdates = true;
-                }
-            }
-        }
-
-        if (needsCurveUpdates) {
-            pendingAnim.addOnFrameCallback(this::updateCurveProperties);
-        }
-
-        pendingAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                // TODO(b/186800707) Figure out how to undo for grid view
-                //  Need to handle cases where dismissed task is
-                //  * Top Row
-                //  * Bottom Row
-                //  * Focused Task
-                updateGridProperties();
-                resetFromSplitSelectionState();
-                updateScrollSynchronously();
-            }
-        });
-
-        return pendingAnim;
-    }
-
     /** TODO(b/181707736) More gracefully handle exiting split selection state */
     private void resetFromSplitSelectionState() {
+        if (mSplitHiddenTaskViewIndex == -1) {
+            return;
+        }
         if (!mActivity.getDeviceProfile().overviewShowAsGrid) {
             int pageToSnapTo = mCurrentPage;
             if (mSplitHiddenTaskViewIndex <= pageToSnapTo) {
@@ -4053,6 +4019,41 @@
         }
     }
 
+    /**
+     * Returns how much additional translation there should be for each of the child TaskViews.
+     * Note that the translation can be its primary or secondary dimension.
+     */
+    public float getSplitSelectTranslation() {
+        int splitPosition = getSplitPlaceholder().getActiveSplitStagePosition();
+        if (!shouldShiftThumbnailsForSplitSelect()) {
+            return 0f;
+        }
+        PagedOrientationHandler orientationHandler = getPagedOrientationHandler();
+        int direction = orientationHandler.getSplitTranslationDirectionFactor(
+                splitPosition, mActivity.getDeviceProfile());
+        return mActivity.getResources().getDimension(R.dimen.split_placeholder_size) * direction;
+    }
+
+    protected void onRotateInSplitSelectionState() {
+        mOrientationHandler.getInitialSplitPlaceholderBounds(mSplitPlaceholderSize,
+                mActivity.getDeviceProfile(),
+                mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect);
+        mTempRectF.set(mTempRect);
+        // TODO(194414938) set correct corner radius
+        mFirstFloatingTaskView.updateOrientationHandler(mOrientationHandler);
+        mFirstFloatingTaskView.update(mTempRectF, /*progress=*/1f, /*windowRadius=*/0f);
+
+        PagedOrientationHandler orientationHandler = getPagedOrientationHandler();
+        Pair<FloatProperty, FloatProperty> taskViewsFloat =
+                orientationHandler.getSplitSelectTaskOffset(
+                        TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
+                        mActivity.getDeviceProfile());
+        taskViewsFloat.first.set(this, getSplitSelectTranslation());
+        taskViewsFloat.second.set(this, 0f);
+
+        applySplitPrimaryScrollOffset();
+    }
+
     private void updateDeadZoneRects() {
         // Get the deadzone rect surrounding the clear all button to not dismiss overview to home
         mClearAllButtonDeadZoneRect.setEmpty();
@@ -4067,8 +4068,8 @@
         mTaskViewDeadZoneRect.setEmpty();
         int count = getTaskViewCount();
         if (count > 0) {
-            final View taskView = getTaskViewAt(0);
-            getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
+            final View taskView = requireTaskViewAt(0);
+            requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect);
             mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(),
                     taskView.getBottom());
         }
@@ -4549,7 +4550,7 @@
 
         final int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
+            TaskView taskView = requireTaskViewAt(i);
             float scrollDiff = taskView.getScrollAdjustment(showAsFullscreen, showAsGrid);
             int pageScroll = newPageScrolls[i] + (int) scrollDiff;
             if ((mIsRtl && pageScroll < clearAllScroll + clearAllWidth)
@@ -4717,7 +4718,7 @@
         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
+            requireTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
         }
     }
 
@@ -4871,7 +4872,7 @@
         mColorTint = tintAmount;
 
         for (int i = 0; i < getTaskViewCount(); i++) {
-            getTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
+            requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor);
         }
 
         Drawable scrimBg = mActivity.getScrimView().getBackground();
@@ -4940,6 +4941,62 @@
     }
 
     @Override
+    public boolean scrollLeft() {
+        if (!showAsGrid()) {
+            return super.scrollLeft();
+        }
+
+        int targetPage = getNextPage();
+        if (targetPage >= 0) {
+            // Find the next page that is not fully visible.
+            TaskView taskView = getTaskViewAt(targetPage);
+            while ((taskView == null || isTaskViewFullyVisible(taskView)) && targetPage - 1 >= 0) {
+                taskView = getTaskViewAt(--targetPage);
+            }
+            // Target a scroll where targetPage is on left of screen but still fully visible.
+            int lastTaskEnd = (mIsRtl
+                    ? mLastComputedGridSize.left
+                    : mLastComputedGridSize.right)
+                    + (mIsRtl ? mPageSpacing : -mPageSpacing);
+            int normalTaskEnd = mIsRtl
+                    ? mLastComputedGridTaskSize.left
+                    : mLastComputedGridTaskSize.right;
+            int targetScroll = getScrollForPage(targetPage) + normalTaskEnd - lastTaskEnd;
+            // Find a page that is close to targetScroll while not over it.
+            while (targetPage - 1 >= 0
+                    && (mIsRtl
+                    ? getScrollForPage(targetPage - 1) < targetScroll
+                    : getScrollForPage(targetPage - 1) > targetScroll)) {
+                targetPage--;
+            }
+            snapToPage(targetPage);
+            return true;
+        }
+
+        return mAllowOverScroll;
+    }
+
+    @Override
+    public boolean scrollRight() {
+        if (!showAsGrid()) {
+            return super.scrollRight();
+        }
+
+        int targetPage = getNextPage();
+        if (targetPage < getChildCount()) {
+            // Find the next page that is not fully visible.
+            TaskView taskView = getTaskViewAt(targetPage);
+            while ((taskView != null && isTaskViewFullyVisible(taskView))
+                    && targetPage + 1 < getChildCount()) {
+                taskView = getTaskViewAt(++targetPage);
+            }
+            snapToPage(targetPage);
+            return true;
+        }
+        return mAllowOverScroll;
+    }
+
+    @Override
     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
         super.onScrollChanged(l, t, oldl, oldt);
         dispatchScrollChanged();
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index d91669a..d833877 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -19,6 +19,7 @@
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
+import static com.android.launcher3.Utilities.comp;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
@@ -290,21 +291,31 @@
             float cornerRadius) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
-                canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
-                canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
+                // TODO(b/189265196): Temporary fix to align the surface with the cutout perfectly.
+                // Round up only when the live tile task is displayed in Overview.
+                float rounding = comp(mFullscreenParams.mFullscreenProgress);
+                float left = x + rounding / 2;
+                float top = y + rounding / 2;
+                float right = width - rounding;
+                float bottom = height - rounding;
+
+                canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
+                        mClearPaint);
+                canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
                         mDimmingPaintAfterClearing);
                 return;
             }
         }
 
-        // Draw the background in all cases, except when the thumbnail data is opaque
+        // Always draw the background since the snapshots might be translucent or partially empty
+        // (For example, tasks been reparented out of dismissing split root when drag-to-dismiss
+        // split screen).
+        canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
+
         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
                 || mThumbnailData == null;
-        if (drawBackgroundOnly || mThumbnailData.isTranslucent) {
-            canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
-            if (drawBackgroundOnly) {
-                return;
-            }
+        if (drawBackgroundOnly) {
+            return;
         }
 
         canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index e8077cf..e33d650 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -599,7 +599,9 @@
         if (confirmSecondSplitSelectApp()) {
             return;
         }
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
+        RecentsView recentsView = getRecentsView();
+        RemoteTargetHandle[] remoteTargetHandles = recentsView.mRemoteTargetHandles;
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask() && remoteTargetHandles != null) {
             if (!mIsClickableAsLiveTile) {
                 return;
             }
@@ -612,9 +614,7 @@
             }
 
             mIsClickableAsLiveTile = false;
-            RecentsView recentsView = getRecentsView();
             RemoteAnimationTargets targets;
-            RemoteTargetHandle[] remoteTargetHandles = recentsView.mRemoteTargetHandles;
             if (remoteTargetHandles.length == 1) {
                 targets = remoteTargetHandles[0].getTransformParams().getTargetSet();
             } else {
@@ -677,7 +677,7 @@
      *         second app. {@code false} otherwise
      */
     private boolean confirmSecondSplitSelectApp() {
-        boolean isSelectingSecondSplitApp = mActivity.isInState(OVERVIEW_SPLIT_SELECT);
+        boolean isSelectingSecondSplitApp = getRecentsView().isSplitSelectionActive();
         if (isSelectingSecondSplitApp) {
             getRecentsView().confirmSplitSelect(this);
         }
@@ -912,7 +912,7 @@
         return deviceProfile.overviewShowAsGrid && !isFocusedTask();
     }
 
-    private void setIconAndDimTransitionProgress(float progress, boolean invert) {
+    protected void setIconAndDimTransitionProgress(float progress, boolean invert) {
         if (invert) {
             progress = 1 - progress;
         }
@@ -1531,6 +1531,7 @@
         private final float mCornerRadius;
         private final float mWindowCornerRadius;
 
+        public float mFullscreenProgress;
         public RectF mCurrentDrawnInsets = new RectF();
         public float mCurrentDrawnCornerRadius;
         /** The current scale we apply to the thumbnail to adjust for new left/right insets. */
@@ -1548,6 +1549,8 @@
          */
         public void setProgress(float fullscreenProgress, float parentScale, float taskViewScale,
                 int previewWidth, DeviceProfile dp, PreviewPositionHelper pph) {
+            mFullscreenProgress = fullscreenProgress;
+
             RectF insets = pph.getInsetsToDrawInFullscreen(dp);
 
             float currentInsetsLeft = insets.left * fullscreenProgress;
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
new file mode 100644
index 0000000..ba1a60d
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -0,0 +1,159 @@
+package com.android.launcher3.taskbar;
+
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD;
+import static com.android.quickstep.OverviewCommandHelper.TYPE_HOME;
+import static com.android.quickstep.OverviewCommandHelper.TYPE_TOGGLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Handler;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TouchInteractionService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class TaskbarNavButtonControllerTest {
+
+    private final static int DISPLAY_ID = 2;
+
+    @Mock
+    SystemUiProxy mockSystemUiProxy;
+    @Mock
+    TouchInteractionService mockService;
+    @Mock
+    OverviewCommandHelper mockCommandHelper;
+    @Mock
+    Handler mockHandler;
+
+    private TaskbarNavButtonController mNavButtonController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mockService.getDisplayId()).thenReturn(DISPLAY_ID);
+        when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper);
+        mNavButtonController = new TaskbarNavButtonController(mockService,
+                mockSystemUiProxy, mockHandler);
+    }
+
+    @Test
+    public void testPressBack() {
+        mNavButtonController.onButtonClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(1)).onBackPressed();
+    }
+
+    @Test
+    public void testPressImeSwitcher() {
+        mNavButtonController.onButtonClick(BUTTON_IME_SWITCH);
+        verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed();
+    }
+
+    @Test
+    public void testPressA11yShortClick() {
+        mNavButtonController.onButtonClick(BUTTON_A11Y);
+        verify(mockSystemUiProxy, times(1))
+                .notifyAccessibilityButtonClicked(DISPLAY_ID);
+    }
+
+    @Test
+    public void testPressA11yLongClick() {
+        mNavButtonController.onButtonLongClick(BUTTON_A11Y);
+        verify(mockSystemUiProxy, times(1)).notifyAccessibilityButtonLongClicked();
+    }
+
+    @Test
+    public void testLongPressHome() {
+        mNavButtonController.onButtonLongClick(BUTTON_HOME);
+        verify(mockSystemUiProxy, times(1)).startAssistant(any());
+    }
+
+    @Test
+    public void testPressHome() {
+        mNavButtonController.onButtonClick(BUTTON_HOME);
+        verify(mockCommandHelper, times(1)).addCommand(TYPE_HOME);
+    }
+
+    @Test
+    public void testPressRecents() {
+        mNavButtonController.onButtonClick(BUTTON_RECENTS);
+        verify(mockCommandHelper, times(1)).addCommand(TYPE_TOGGLE);
+    }
+
+    @Test
+    public void testPressRecentsWithScreenPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonClick(BUTTON_RECENTS);
+        verify(mockCommandHelper, times(0)).addCommand(TYPE_TOGGLE);
+    }
+
+    @Test
+    public void testLongPressBackRecentsNotPinned() {
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(0)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressBackRecentsPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(1)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressBackRecentsTooLongPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        try {
+            Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(0)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressBackRecentsMultipleAttemptPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        try {
+            Thread.sleep(SCREEN_PIN_LONG_PRESS_THRESHOLD + 5);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(0)).stopScreenPinning();
+
+        // Try again w/in threshold
+        mNavButtonController.onButtonLongClick(BUTTON_RECENTS);
+        mNavButtonController.onButtonLongClick(BUTTON_BACK);
+        verify(mockSystemUiProxy, times(1)).stopScreenPinning();
+    }
+
+    @Test
+    public void testLongPressHomeScreenPinned() {
+        mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
+        mNavButtonController.onButtonLongClick(BUTTON_HOME);
+        verify(mockSystemUiProxy, times(0)).startAssistant(any());
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 93be6fb..7c503be 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -324,14 +324,14 @@
         if (!mLauncher.isTablet()) {
             return;
         }
-        for (int i = 2; i <= 12; i++) {
+        for (int i = 2; i <= 14; i++) {
             startTestActivity(i);
         }
 
         Overview overview = mLauncher.pressHome().switchToOverview();
         executeOnLauncher(
-                launcher -> assertTrue("Don't have at least 11 tasks",
-                        getTaskCount(launcher) >= 11));
+                launcher -> assertTrue("Don't have at least 13 tasks",
+                        getTaskCount(launcher) >= 13));
 
         // Test scroll the first task off screen
         overview.scrollCurrentTaskOffScreen();
diff --git a/res/drawable/ic_split_horizontal.xml b/res/drawable/ic_split_horizontal.xml
new file mode 100644
index 0000000..ee710d0
--- /dev/null
+++ b/res/drawable/ic_split_horizontal.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="16dp"
+    android:viewportWidth="20"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M18,14L13,14L13,2L18,2L18,14ZM20,14L20,2C20,0.9 19.1,-0 18,-0L13,-0C11.9,-0 11,0.9 11,2L11,14C11,15.1 11.9,16 13,16L18,16C19.1,16 20,15.1 20,14ZM7,14L2,14L2,2L7,2L7,14ZM9,14L9,2C9,0.9 8.1,-0 7,-0L2,-0C0.9,-0 -0,0.9 -0,2L-0,14C-0,15.1 0.9,16 2,16L7,16C8.1,16 9,15.1 9,14Z"
+      android:fillColor="#000000"/>
+</vector>
diff --git a/res/drawable/ic_split_left.xml b/res/drawable/ic_split_left.xml
new file mode 100644
index 0000000..fc9f699
--- /dev/null
+++ b/res/drawable/ic_split_left.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="16dp"
+    android:viewportWidth="20"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M-0,2L-0,14C-0,15.1 0.9,16 2,16L7,16C8.1,16 9,15.1 9,14L9,2C9,0.9 8.1,-0 7,-0L2,-0C0.9,-0 -0,0.9 -0,2ZM13,2L18,2L18,14L13,14L13,2ZM11,2L11,14C11,15.1 11.9,16 13,16L18,16C19.1,16 20,15.1 20,14L20,2C20,0.9 19.1,-0 18,-0L13,-0C11.9,-0 11,0.9 11,2Z"
+      android:fillColor="#000000"/>
+</vector>
diff --git a/res/drawable/ic_split_right.xml b/res/drawable/ic_split_right.xml
new file mode 100644
index 0000000..cc15622
--- /dev/null
+++ b/res/drawable/ic_split_right.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="16dp"
+    android:viewportWidth="20"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M20,14L20,2C20,0.9 19.1,-0 18,-0L13,-0C11.9,-0 11,0.9 11,2L11,14C11,15.1 11.9,16 13,16L18,16C19.1,16 20,15.1 20,14ZM7,14L2,14L2,2L7,2L7,14ZM9,14L9,2C9,0.9 8.1,-0 7,-0L2,-0C0.9,-0 -0,0.9 -0,2L-0,14C-0,15.1 0.9,16 2,16L7,16C8.1,16 9,15.1 9,14Z"
+      android:fillColor="#000000"/>
+</vector>
diff --git a/res/drawable/ic_split_top.xml b/res/drawable/ic_split_top.xml
new file mode 100644
index 0000000..f8c15bd
--- /dev/null
+++ b/res/drawable/ic_split_top.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="20dp"
+    android:viewportWidth="16"
+    android:viewportHeight="20">
+  <path
+      android:pathData="M14,0H2C0.9,0 0,0.9 0,2V7C0,8.1 0.9,9 2,9H14C15.1,9 16,8.1 16,7V2C16,0.9 15.1,0 14,0ZM14,13V18H2V13H14ZM14,11H2C0.9,11 0,11.9 0,13V18C0,19.1 0.9,20 2,20H14C15.1,20 16,19.1 16,18V13C16,11.9 15.1,11 14,11Z"
+      android:fillColor="#000000"/>
+</vector>
diff --git a/res/drawable/ic_split_vertical.xml b/res/drawable/ic_split_vertical.xml
new file mode 100644
index 0000000..9bc9785
--- /dev/null
+++ b/res/drawable/ic_split_vertical.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M18,4V9H6V4H18ZM18,2H6C4.9,2 4,2.9 4,4V9C4,10.1 4.9,11 6,11H18C19.1,11 20,10.1 20,9V4C20,2.9 19.1,2 18,2ZM18,15V20H6V15H18ZM18,13H6C4.9,13 4,13.9 4,15V20C4,21.1 4.9,22 6,22H18C19.1,22 20,21.1 20,20V15C20,13.9 19.1,13 18,13Z"
+      android:fillColor="#000000"/>
+</vector>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 08f0089..08570eb 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -154,7 +154,15 @@
         <attr name="demoModeLayoutId" format="reference" />
         <attr name="isScalable" format="boolean" />
         <attr name="devicePaddingId" format="reference" />
-        <attr name="gridEnabled" format="boolean" />
+        <!-- By default all categories are enabled -->
+        <attr name="deviceCategory" format="integer" >
+            <!-- Enable on phone only -->
+            <flag name="phone" value="1" />
+            <!-- Enable on tablets only -->
+            <flag name="tablet" value="2" />
+            <!-- Enable on multi display devices only -->
+            <flag name="multi_display" value="4" />
+        </attr>
 
     </declare-styleable>
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 78f3f11..6d223e7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -300,6 +300,7 @@
     <dimen name="snackbar_elevation">3dp</dimen>
     <dimen name="snackbar_min_text_size">12sp</dimen>
     <dimen name="snackbar_max_text_size">14sp</dimen>
+    <dimen name="snackbar_max_width">504dp</dimen>
 
 <!-- Developer Options -->
     <dimen name="developer_options_filter_margins">10dp</dimen>
@@ -321,6 +322,9 @@
     <!-- Size of the maximum radius for the enforced rounded rectangles. -->
     <dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
 
+<!-- Base Swipe Detector, speed in dp/s -->
+    <dimen name="base_swift_detector_fling_release_velocity">1dp</dimen>
+
 <!-- Overview placeholder to compile in Launcher3 without Quickstep -->
     <dimen name="task_thumbnail_icon_size">0dp</dimen>
     <dimen name="task_thumbnail_icon_drawable_size">0dp</dimen>
@@ -328,6 +332,8 @@
     <dimen name="overview_task_margin">0dp</dimen>
     <dimen name="overview_task_margin_focused">0dp</dimen>
     <dimen name="overview_task_margin_grid">0dp</dimen>
+    <dimen name="overview_actions_button_spacing">0dp</dimen>
+    <dimen name="overview_actions_button_spacing_grid">0dp</dimen>
     <dimen name="overview_actions_margin_gesture">0dp</dimen>
     <dimen name="overview_actions_top_margin_gesture_grid_portrait">0dp</dimen>
     <dimen name="overview_actions_bottom_margin_gesture_grid_portrait">0dp</dimen>
@@ -336,10 +342,10 @@
     <dimen name="overview_actions_margin_three_button">0dp</dimen>
     <dimen name="overview_grid_side_margin_portrait">0dp</dimen>
     <dimen name="overview_grid_side_margin_landscape">0dp</dimen>
-    <dimen name="overview_grid_row_spacing_portrait">0dp</dimen>
-    <dimen name="overview_grid_row_spacing_landscape">0dp</dimen>
-    <dimen name="recents_page_spacing">0dp</dimen>
-    <dimen name="recents_page_spacing_grid">0dp</dimen>
+    <dimen name="overview_grid_row_spacing">0dp</dimen>
+    <dimen name="overview_page_spacing">0dp</dimen>
+    <dimen name="overview_page_spacing_grid_portrait">0dp</dimen>
+    <dimen name="overview_page_spacing_grid_landscape">0dp</dimen>
     <dimen name="split_placeholder_size">110dp</dimen>
     <dimen name="task_menu_width_grid">200dp</dimen>
 
diff --git a/res/xml/default_workspace_6x5.xml b/res/xml/default_workspace_6x5.xml
new file mode 100644
index 0000000..b078cfd
--- /dev/null
+++ b/res/xml/default_workspace_6x5.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
+
+    <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
+    <!-- Mail Calendar Gallery Store Internet Camera -->
+    <resolve
+        launcher:container="-101"
+        launcher:screen="0"
+        launcher:x="0"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
+        <favorite launcher:uri="mailto:" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="1"
+        launcher:x="1"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_CALENDAR;end" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="2"
+        launcher:x="2"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
+        <favorite launcher:uri="#Intent;type=images/*;end" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="3"
+        launcher:x="3"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
+        <favorite launcher:uri="market://details?id=com.android.launcher" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="4"
+        launcher:x="4"
+        launcher:y="0" >
+        <favorite
+            launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
+        <favorite launcher:uri="http://www.example.com/" />
+    </resolve>
+
+    <!-- Resolve camera intent if GoogleCamera is not available e.g. on emulator -->
+    <resolve
+        launcher:container="-101"
+        launcher:screen="5"
+        launcher:x="5"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
+        <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
+    </resolve>
+
+</favorites>
diff --git a/res/xml/device_profiles.xml b/res/xml/device_profiles.xml
index e030f81..b4bb43e 100644
--- a/res/xml/device_profiles.xml
+++ b/res/xml/device_profiles.xml
@@ -133,4 +133,32 @@
 
     </grid-option>
 
+    <grid-option
+        launcher:name="6_by_5"
+        launcher:numRows="5"
+        launcher:numColumns="6"
+        launcher:numFolderRows="3"
+        launcher:numFolderColumns="3"
+        launcher:numHotseatIcons="6"
+        launcher:numAllAppsColumns="6"
+        launcher:dbFile="launcher_6_by_5.db"
+        launcher:defaultLayoutId="@xml/default_workspace_6x5"
+        launcher:deviceCategory="tablet">
+
+        <display-option
+            launcher:name="Tablet"
+            launcher:minWidthDps="900"
+            launcher:minHeightDps="820"
+            launcher:minCellHeightDps="104"
+            launcher:minCellWidthDps="80"
+            launcher:iconImageSize="60"
+            launcher:iconTextSize="14"
+            launcher:borderSpaceDps="16"
+            launcher:allAppsIconSize="60"
+            launcher:allAppsIconTextSize="14"
+            launcher:allAppsCellSpacingDps="16"
+            launcher:canBeDefault="true" />
+
+    </grid-option>
+
 </profiles>
\ No newline at end of file
diff --git a/res/xml/device_profiles_split.xml b/res/xml/device_profiles_split.xml
deleted file mode 100644
index 2fad0c9..0000000
--- a/res/xml/device_profiles_split.xml
+++ /dev/null
@@ -1,137 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2021 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.
--->
-
-<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
-
-    <grid-option
-        launcher:name="3_by_3"
-        launcher:numRows="3"
-        launcher:numColumns="3"
-        launcher:numFolderRows="2"
-        launcher:numFolderColumns="3"
-        launcher:numHotseatIcons="3"
-        launcher:dbFile="launcher_3_by_3.db"
-        launcher:defaultLayoutId="@xml/default_workspace_3x3" >
-
-        <display-option
-            launcher:name="Super Short Stubby"
-            launcher:minWidthDps="255"
-            launcher:minHeightDps="300"
-            launcher:iconImageSize="48"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-        <display-option
-            launcher:name="Shorter Stubby"
-            launcher:minWidthDps="255"
-            launcher:minHeightDps="400"
-            launcher:iconImageSize="48"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-    </grid-option>
-
-    <grid-option
-        launcher:name="4_by_4"
-        launcher:numRows="4"
-        launcher:numColumns="4"
-        launcher:numFolderRows="3"
-        launcher:numFolderColumns="4"
-        launcher:numHotseatIcons="4"
-        launcher:dbFile="launcher_4_by_4.db"
-        launcher:defaultLayoutId="@xml/default_workspace_4x4" >
-
-        <display-option
-            launcher:name="Short Stubby"
-            launcher:minWidthDps="275"
-            launcher:minHeightDps="420"
-            launcher:iconImageSize="48"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-        <display-option
-            launcher:name="Stubby"
-            launcher:minWidthDps="255"
-            launcher:minHeightDps="450"
-            launcher:iconImageSize="48"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-        <display-option
-            launcher:name="Nexus S"
-            launcher:minWidthDps="296"
-            launcher:minHeightDps="491.33"
-            launcher:iconImageSize="48"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-        <display-option
-            launcher:name="Nexus 4"
-            launcher:minWidthDps="359"
-            launcher:minHeightDps="567"
-            launcher:iconImageSize="54"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-        <display-option
-            launcher:name="Nexus 5"
-            launcher:minWidthDps="335"
-            launcher:minHeightDps="567"
-            launcher:iconImageSize="54"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-    </grid-option>
-
-    <grid-option
-        launcher:name="5_by_5"
-        launcher:numRows="5"
-        launcher:numColumns="5"
-        launcher:numFolderRows="4"
-        launcher:numFolderColumns="4"
-        launcher:numHotseatIcons="5"
-        launcher:numExtendedHotseatIcons="8"
-        launcher:dbFile="launcher.db"
-        launcher:defaultLayoutId="@xml/default_workspace_5x5" >
-
-        <display-option
-            launcher:name="Large Phone"
-            launcher:minWidthDps="406"
-            launcher:minHeightDps="694"
-            launcher:iconImageSize="56"
-            launcher:iconTextSize="14.4"
-            launcher:canBeDefault="true" />
-
-        <display-option
-            launcher:name="Large Phone Split Display"
-            launcher:minWidthDps="406"
-            launcher:minHeightDps="694"
-            launcher:iconImageSize="56"
-            launcher:iconTextSize="14.4"
-            launcher:canBeDefault="true" />
-
-        <display-option
-            launcher:name="Shorter Stubby"
-            launcher:minWidthDps="255"
-            launcher:minHeightDps="400"
-            launcher:iconImageSize="48"
-            launcher:iconTextSize="13.0"
-            launcher:canBeDefault="true" />
-
-    </grid-option>
-
-</profiles>
\ No newline at end of file
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index ff19918..6a399ac 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -187,6 +187,7 @@
     public final int overviewActionsMarginThreeButtonPx;
     public final int overviewActionsTopMarginGesturePx;
     public final int overviewActionsBottomMarginGesturePx;
+    public final int overviewActionsButtonSpacing;
     public int overviewPageSpacing;
     public int overviewRowSpacing;
     public int overviewGridSideMargin;
@@ -371,25 +372,29 @@
                         R.dimen.overview_actions_top_margin_gesture_grid_landscape);
                 overviewActionsBottomMarginGesturePx = res.getDimensionPixelSize(
                         R.dimen.overview_actions_bottom_margin_gesture_grid_landscape);
+                overviewPageSpacing = res.getDimensionPixelSize(
+                        R.dimen.overview_page_spacing_grid_landscape);
             } else {
                 overviewActionsTopMarginGesturePx = res.getDimensionPixelSize(
                         R.dimen.overview_actions_top_margin_gesture_grid_portrait);
                 overviewActionsBottomMarginGesturePx = res.getDimensionPixelSize(
                         R.dimen.overview_actions_bottom_margin_gesture_grid_portrait);
+                overviewPageSpacing = res.getDimensionPixelSize(
+                        R.dimen.overview_page_spacing_grid_portrait);
             }
+            overviewActionsButtonSpacing = res.getDimensionPixelSize(
+                    R.dimen.overview_actions_button_spacing_grid);
         } else {
             overviewActionsTopMarginGesturePx = res.getDimensionPixelSize(
                     R.dimen.overview_actions_margin_gesture);
             overviewActionsBottomMarginGesturePx = overviewActionsTopMarginGesturePx;
+            overviewActionsButtonSpacing = res.getDimensionPixelSize(
+                    R.dimen.overview_actions_button_spacing);
+            overviewPageSpacing = res.getDimensionPixelSize(R.dimen.overview_page_spacing);
         }
         overviewActionsMarginThreeButtonPx = res.getDimensionPixelSize(
                 R.dimen.overview_actions_margin_three_button);
-        overviewPageSpacing = overviewShowAsGrid
-                ? res.getDimensionPixelSize(R.dimen.recents_page_spacing_grid)
-                : res.getDimensionPixelSize(R.dimen.recents_page_spacing);
-        overviewRowSpacing = isLandscape
-                ? res.getDimensionPixelSize(R.dimen.overview_grid_row_spacing_landscape)
-                : res.getDimensionPixelSize(R.dimen.overview_grid_row_spacing_portrait);
+        overviewRowSpacing = res.getDimensionPixelSize(R.dimen.overview_grid_row_spacing);
         overviewGridSideMargin = isLandscape
                 ? res.getDimensionPixelSize(R.dimen.overview_grid_side_margin_landscape)
                 : res.getDimensionPixelSize(R.dimen.overview_grid_side_margin_portrait);
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 2ebedf7..ff7a90c 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -42,6 +42,7 @@
 import android.util.Xml;
 import android.view.Display;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
@@ -58,6 +59,8 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -70,6 +73,13 @@
     public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
             new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
 
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({TYPE_PHONE, TYPE_MULTI_DISPLAY, TYPE_TABLET})
+    public @interface DeviceType{}
+    public static final int TYPE_PHONE = 0;
+    public static final int TYPE_MULTI_DISPLAY = 1;
+    public static final int TYPE_TABLET = 2;
+
     private static final String KEY_IDP_GRID_NAME = "idp_grid_name";
 
     private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
@@ -106,7 +116,7 @@
     public float[] iconTextSize;
     public int iconBitmapSize;
     public int fillResIconDpi;
-    public boolean isSplitDisplay;
+    public @DeviceType int deviceType;
 
     public PointF[] minCellSize;
 
@@ -198,13 +208,21 @@
         String gridName = getCurrentGridName(context);
 
         // Get the display info based on default display and interpolate it to existing display
+        Info defaultInfo = DisplayController.INSTANCE.get(context).getInfo();
+        @DeviceType int defaultDeviceType = getDeviceType(defaultInfo);
         DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
-                DisplayController.INSTANCE.get(context).getInfo(),
-                getPredefinedDeviceProfiles(context, gridName, false, false), false);
+                defaultInfo,
+                getPredefinedDeviceProfiles(context, gridName, defaultDeviceType,
+                        /*allowDisabledGrid=*/false),
+                defaultDeviceType);
 
         Info myInfo = new Info(context, display);
+        @DeviceType int deviceType = getDeviceType(myInfo);
         DisplayOption myDisplayOption = invDistWeightedInterpolate(
-                myInfo, getPredefinedDeviceProfiles(context, gridName, false, false), false);
+                myInfo,
+                getPredefinedDeviceProfiles(context, gridName, deviceType,
+                        /*allowDisabledGrid=*/false),
+                deviceType);
 
         DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
                 .add(myDisplayOption);
@@ -220,7 +238,7 @@
         System.arraycopy(defaultDisplayOption.borderSpaces, 0, result.borderSpaces, 0,
                 COUNT_SIZES);
 
-        initGrid(context, myInfo, result, false);
+        initGrid(context, myInfo, result, deviceType);
     }
 
     /**
@@ -246,6 +264,18 @@
         }
     }
 
+    private static @DeviceType int getDeviceType(Info displayInfo) {
+        // Each screen has two profiles (portrait/landscape), so devices with four or more
+        // supported profiles implies two or more internal displays.
+        if (displayInfo.supportedBounds.size() >= 4 && ENABLE_TWO_PANEL_HOME.get()) {
+            return TYPE_MULTI_DISPLAY;
+        } else if (displayInfo.supportedBounds.stream().allMatch(displayInfo::isTablet)) {
+            return TYPE_TABLET;
+        } else {
+            return TYPE_PHONE;
+        }
+    }
+
     public static String getCurrentGridName(Context context) {
         return Utilities.isGridOptionsEnabled(context)
                 ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null;
@@ -253,23 +283,19 @@
 
     private String initGrid(Context context, String gridName) {
         Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
-        // Each screen has two profiles (portrait/landscape), so devices with four or more
-        // supported profiles implies two or more internal displays.
-        boolean isSplitDisplay =
-                displayInfo.supportedBounds.size() >= 4 && ENABLE_TWO_PANEL_HOME.get();
+        @DeviceType int deviceType = getDeviceType(displayInfo);
 
         ArrayList<DisplayOption> allOptions =
-                getPredefinedDeviceProfiles(context, gridName, isSplitDisplay,
+                getPredefinedDeviceProfiles(context, gridName, deviceType,
                         RestoreDbTask.isPending(context));
         DisplayOption displayOption =
-                invDistWeightedInterpolate(displayInfo, allOptions, isSplitDisplay);
-        initGrid(context, displayInfo, displayOption, isSplitDisplay);
+                invDistWeightedInterpolate(displayInfo, allOptions, deviceType);
+        initGrid(context, displayInfo, displayOption, deviceType);
         return displayOption.grid.name;
     }
 
-    private void initGrid(
-            Context context, Info displayInfo, DisplayOption displayOption,
-            boolean isSplitDisplay) {
+    private void initGrid(Context context, Info displayInfo, DisplayOption displayOption,
+            @DeviceType int deviceType) {
         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
         GridOption closestProfile = displayOption.grid;
         numRows = closestProfile.numRows;
@@ -281,7 +307,7 @@
         numFolderColumns = closestProfile.numFolderColumns;
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
-        this.isSplitDisplay = isSplitDisplay;
+        this.deviceType = deviceType;
 
         mExtraAttrs = closestProfile.extraAttrs;
 
@@ -303,11 +329,11 @@
         horizontalMargin = displayOption.horizontalMargin;
 
         numShownHotseatIcons = closestProfile.numHotseatIcons;
-        numDatabaseHotseatIcons = isSplitDisplay
+        numDatabaseHotseatIcons = deviceType == TYPE_MULTI_DISPLAY
                 ? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons;
 
         numAllAppsColumns = closestProfile.numAllAppsColumns;
-        numDatabaseAllAppsColumns = isSplitDisplay
+        numDatabaseAllAppsColumns = deviceType == TYPE_MULTI_DISPLAY
                 ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns;
 
         if (!Utilities.isGridOptionsEnabled(context)) {
@@ -327,7 +353,7 @@
         defaultWallpaperSize = new Point(displayInfo.currentSize);
         for (WindowBounds bounds : displayInfo.supportedBounds) {
             localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
-                    .setUseTwoPanels(isSplitDisplay)
+                    .setUseTwoPanels(deviceType == TYPE_MULTI_DISPLAY)
                     .setWindowBounds(bounds).build());
 
             // Wallpaper size should be the maximum of the all possible sizes Launcher expects
@@ -350,11 +376,6 @@
         defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
     }
 
-    @Nullable
-    public TypedValue getAttrValue(int attr) {
-        return mExtraAttrs == null ? null : mExtraAttrs.get(attr);
-    }
-
     public void addOnChangeListener(OnIDPChangeListener listener) {
         mChangeListeners.add(listener);
     }
@@ -389,12 +410,11 @@
         }
     }
 
-    private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(
-            Context context, String gridName, boolean isSplitDisplay, boolean allowDisabledGrid) {
+    private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
+            String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
-        int xmlResource = isSplitDisplay ? R.xml.device_profiles_split : R.xml.device_profiles;
 
-        try (XmlResourceParser parser = context.getResources().getXml(xmlResource)) {
+        try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
             int type;
             while (((type = parser.next()) != XmlPullParser.END_TAG ||
@@ -402,8 +422,8 @@
                 if ((type == XmlPullParser.START_TAG)
                         && GridOption.TAG_NAME.equals(parser.getName())) {
 
-                    GridOption gridOption =
-                            new GridOption(context, Xml.asAttributeSet(parser), isSplitDisplay);
+                    GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser),
+                            deviceType);
                     if (gridOption.isEnabled || allowDisabledGrid) {
                         final int displayDepth = parser.getDepth();
                         while (((type = parser.next()) != XmlPullParser.END_TAG
@@ -450,9 +470,8 @@
      */
     public List<GridOption> parseAllGridOptions(Context context) {
         List<GridOption> result = new ArrayList<>();
-        int xmlResource = isSplitDisplay ? R.xml.device_profiles_split : R.xml.device_profiles;
 
-        try (XmlResourceParser parser = context.getResources().getXml(xmlResource)) {
+        try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
             int type;
             while (((type = parser.next()) != XmlPullParser.END_TAG
@@ -460,7 +479,7 @@
                 if ((type == XmlPullParser.START_TAG)
                         && GridOption.TAG_NAME.equals(parser.getName())) {
                     GridOption option =
-                            new GridOption(context, Xml.asAttributeSet(parser), isSplitDisplay);
+                            new GridOption(context, Xml.asAttributeSet(parser), deviceType);
                     if (option.isEnabled) {
                         result.add(option);
                     }
@@ -514,12 +533,12 @@
     }
 
     private static DisplayOption invDistWeightedInterpolate(
-            Info displayInfo, ArrayList<DisplayOption> points, boolean isSplitDisplay) {
+            Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType) {
         int minWidthPx = Integer.MAX_VALUE;
         int minHeightPx = Integer.MAX_VALUE;
         for (WindowBounds bounds : displayInfo.supportedBounds) {
             boolean isTablet = displayInfo.isTablet(bounds);
-            if (isTablet && isSplitDisplay) {
+            if (isTablet && deviceType == TYPE_MULTI_DISPLAY) {
                 // For split displays, take half width per page
                 minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
                 minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
@@ -643,6 +662,12 @@
 
         public static final String TAG_NAME = "grid-option";
 
+        private static final int DEVICE_CATEGORY_PHONE = 1 << 0;
+        private static final int DEVICE_CATEGORY_TABLET = 1 << 1;
+        private static final int DEVICE_CATEGORY_MULTI_DISPLAY = 1 << 2;
+        private static final int DEVICE_CATEGORY_ALL =
+                DEVICE_CATEGORY_PHONE | DEVICE_CATEGORY_TABLET | DEVICE_CATEGORY_MULTI_DISPLAY;
+
         public final String name;
         public final int numRows;
         public final int numColumns;
@@ -666,7 +691,7 @@
 
         private final SparseArray<TypedValue> extraAttrs;
 
-        public GridOption(Context context, AttributeSet attrs, boolean isSplitDisplay) {
+        public GridOption(Context context, AttributeSet attrs, @DeviceType int deviceType) {
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.GridDisplayOption);
             name = a.getString(R.styleable.GridDisplayOption_name);
@@ -674,7 +699,7 @@
             numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
 
             dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
-            defaultLayoutId = a.getResourceId(isSplitDisplay && a.hasValue(
+            defaultLayoutId = a.getResourceId(deviceType == TYPE_MULTI_DISPLAY && a.hasValue(
                     R.styleable.GridDisplayOption_defaultSplitDisplayLayoutId)
                     ? R.styleable.GridDisplayOption_defaultSplitDisplayLayoutId
                     : R.styleable.GridDisplayOption_defaultLayoutId, 0);
@@ -701,7 +726,15 @@
             devicePaddingId = a.getResourceId(
                     R.styleable.GridDisplayOption_devicePaddingId, 0);
 
-            isEnabled = a.getBoolean(R.styleable.GridDisplayOption_gridEnabled, true);
+            int deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
+                    DEVICE_CATEGORY_ALL);
+            isEnabled = (deviceType == TYPE_PHONE
+                        && ((deviceCategory & DEVICE_CATEGORY_PHONE) == DEVICE_CATEGORY_PHONE))
+                    || (deviceType == TYPE_TABLET
+                        && ((deviceCategory & DEVICE_CATEGORY_TABLET) == DEVICE_CATEGORY_TABLET))
+                    || (deviceType == TYPE_MULTI_DISPLAY
+                        && ((deviceCategory & DEVICE_CATEGORY_MULTI_DISPLAY)
+                            == DEVICE_CATEGORY_MULTI_DISPLAY));
 
             a.recycle();
             extraAttrs = Themes.createValueMap(context, attrs,
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index a522b11..81a0d5b 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -29,6 +29,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
@@ -39,6 +40,7 @@
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
+import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
@@ -55,6 +57,7 @@
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
+import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -90,6 +93,7 @@
 import android.os.StrictMode;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.os.UserHandle;
 import android.text.TextUtils;
 import android.text.method.TextKeyListener;
 import android.util.Log;
@@ -215,6 +219,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -2676,6 +2681,79 @@
         }
     }
 
+    /**
+     * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close
+     * animation.
+     *
+     * @param preferredItemId The id of the preferred item to match to if it exists.
+     * @param packageName The package name of the app to match.
+     * @param user The user of the app to match.
+     */
+    public View getFirstMatchForAppClose(int preferredItemId, String packageName, UserHandle user) {
+        final ItemInfoMatcher preferredItem = (info, cn) ->
+                info != null && info.id == preferredItemId;
+        final ItemInfoMatcher packageAndUserAndApp = (info, cn) ->
+                info != null
+                        && info.itemType == ITEM_TYPE_APPLICATION
+                        && info.user.equals(user)
+                        && info.getTargetComponent() != null
+                        && TextUtils.equals(info.getTargetComponent().getPackageName(),
+                        packageName);
+
+        if (isInState(LauncherState.ALL_APPS)) {
+            return getFirstMatch(Collections.singletonList(mAppsView.getActiveRecyclerView()),
+                    preferredItem, packageAndUserAndApp);
+        } else {
+            List<ViewGroup> containers = new ArrayList<>(mWorkspace.getPanelCount() + 1);
+            containers.add(mWorkspace.getHotseat().getShortcutsAndWidgets());
+            mWorkspace.forEachVisiblePage(page
+                    -> containers.add(((CellLayout) page).getShortcutsAndWidgets()));
+
+            // Order: Preferred item by itself or in folder, then by matching package/user
+            if (ADAPTIVE_ICON_WINDOW_ANIM.get()) {
+                return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem),
+                        packageAndUserAndApp, forFolderMatch(packageAndUserAndApp));
+            } else {
+                // Do not use Folder as a criteria, since it'll cause a crash when trying to draw
+                // FolderAdaptiveIcon as the background.
+                return getFirstMatch(containers, preferredItem, packageAndUserAndApp);
+            }
+        }
+    }
+
+    /**
+     * Finds the first view matching the ordered operators across the given viewgroups in order.
+     * @param containers List of ViewGroups to scan, in order of preference.
+     * @param operators List of operators, in order starting from best matching operator.
+     */
+    private static View getFirstMatch(Iterable<ViewGroup> containers,
+            final ItemInfoMatcher... operators) {
+        for (ItemInfoMatcher operator : operators) {
+            for (ViewGroup container : containers) {
+                View match = mapOverViewGroup(container, operator);
+                if (match != null) {
+                    return match;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the first view matching the operator in the given ViewGroups, or null if none.
+     * Forward iteration matters.
+     */
+    private static View mapOverViewGroup(ViewGroup container, ItemInfoMatcher op) {
+        final int itemCount = container.getChildCount();
+        for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
+            View item = container.getChildAt(itemIdx);
+            if (op.matchesInfo((ItemInfo) item.getTag())) {
+                return item;
+            }
+        }
+        return null;
+    }
+
     private ValueAnimator createNewAppBounceAnimation(View v, int i) {
         ValueAnimator bounceAnim = new PropertyListBuilder().alpha(1).scale(1).build(v)
                 .setDuration(ItemInstallQueue.NEW_SHORTCUT_BOUNCE_DURATION);
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 15378e0..be2cd88 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -57,10 +57,9 @@
     public static final int ALL_APPS_CONTENT = 1 << 1;
     public static final int VERTICAL_SWIPE_INDICATOR = 1 << 2;
     public static final int OVERVIEW_ACTIONS = 1 << 3;
-    public static final int TASKBAR = 1 << 4;
-    public static final int CLEAR_ALL_BUTTON = 1 << 5;
-    public static final int WORKSPACE_PAGE_INDICATOR = 1 << 6;
-    public static final int SPLIT_PLACHOLDER_VIEW = 1 << 7;
+    public static final int CLEAR_ALL_BUTTON = 1 << 4;
+    public static final int WORKSPACE_PAGE_INDICATOR = 1 << 5;
+    public static final int SPLIT_PLACHOLDER_VIEW = 1 << 6;
 
     // Flag indicating workspace has multiple pages visible.
     public static final int FLAG_MULTI_PAGE = BaseState.getFlag(0);
@@ -186,7 +185,7 @@
     }
 
     public int getVisibleElements(Launcher launcher) {
-        return HOTSEAT_ICONS | WORKSPACE_PAGE_INDICATOR | VERTICAL_SWIPE_INDICATOR | TASKBAR;
+        return HOTSEAT_ICONS | WORKSPACE_PAGE_INDICATOR | VERTICAL_SWIPE_INDICATOR;
     }
 
     /**
@@ -197,10 +196,16 @@
         return (getVisibleElements(launcher) & elements) == elements;
     }
 
-    public boolean isTaskbarStashed() {
+    /** Returns whether taskbar is stashed and thus should replace hotseat with a handle */
+    public boolean isTaskbarStashed(Launcher launcher) {
         return false;
     }
 
+    /** Returns whether taskbar is aligned with the hotseat vs position inside apps */
+    public boolean isTaskbarAlignedWithHotseat(Launcher launcher) {
+        return !isTaskbarStashed(launcher);
+    }
+
     /**
      * Fraction shift in the vertical translation UI and related properties
      *
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 1ce7ebe..2c14f07 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1749,20 +1749,23 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         final boolean pagesFlipped = isPageOrderFlipped();
-        int offset = (mAllowOverScroll ? 0 : 1);
-        info.setScrollable(getPageCount() > offset);
-        if (getCurrentPage() < getPageCount() - offset) {
+        info.setScrollable(getPageCount() > 0);
+        int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+        if (getCurrentPage() < getPageCount() - getPanelCount()
+                || (getCurrentPage() == getPageCount() - getPanelCount()
+                && primaryScroll != getScrollForPage(getPageCount() - getPanelCount()))) {
             info.addAction(pagesFlipped ?
-                AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD
-                : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+                    AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD
+                    : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
             info.addAction(mIsRtl ?
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT
                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);
         }
-        if (getCurrentPage() >= offset) {
+        if (getCurrentPage() > 0
+                || (getCurrentPage() == 0 && primaryScroll != getScrollForPage(0))) {
             info.addAction(pagesFlipped ?
-                AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD
-                : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+                    AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD
+                    : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
             info.addAction(mIsRtl ?
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT
                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);
@@ -1807,16 +1810,16 @@
             } break;
             case android.R.id.accessibilityActionPageRight: {
                 if (!mIsRtl) {
-                  return scrollRight();
+                    return scrollRight();
                 } else {
-                  return scrollLeft();
+                    return scrollLeft();
                 }
             }
             case android.R.id.accessibilityActionPageLeft: {
                 if (!mIsRtl) {
-                  return scrollLeft();
+                    return scrollLeft();
                 } else {
-                  return scrollRight();
+                    return scrollRight();
                 }
             }
         }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 203df0a..281dfea 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -26,7 +26,6 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
-import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
@@ -51,7 +50,6 @@
 import android.os.Message;
 import android.os.Parcelable;
 import android.os.UserHandle;
-import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.SparseArray;
@@ -3146,62 +3144,6 @@
         return layouts;
     }
 
-    /**
-     * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close
-     * animation.
-     *
-     * @param preferredItemId The id of the preferred item to match to if it exists.
-     * @param packageName The package name of the app to match.
-     * @param user The user of the app to match.
-     */
-    public View getFirstMatchForAppClose(int preferredItemId, String packageName, UserHandle user) {
-        final ItemOperator preferredItem = (ItemInfo info, View view) ->
-                info != null && info.id == preferredItemId;
-        final ItemOperator preferredItemInFolder = (info, view) -> {
-            if (info instanceof FolderInfo) {
-                FolderInfo folderInfo = (FolderInfo) info;
-                for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) {
-                    if (preferredItem.evaluate(shortcutInfo, view)) {
-                        return true;
-                    }
-                }
-            }
-            return false;
-        };
-        final ItemOperator packageAndUserAndApp = (ItemInfo info, View view) ->
-                info != null
-                        && info.itemType == ITEM_TYPE_APPLICATION
-                        && info.user.equals(user)
-                        && info.getTargetComponent() != null
-                        && TextUtils.equals(info.getTargetComponent().getPackageName(),
-                                packageName);
-        final ItemOperator packageAndUserAndAppInFolder = (info, view) -> {
-            if (info instanceof FolderInfo) {
-                FolderInfo folderInfo = (FolderInfo) info;
-                for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) {
-                    if (packageAndUserAndApp.evaluate(shortcutInfo, view)) {
-                        return true;
-                    }
-                }
-            }
-            return false;
-        };
-
-        List<CellLayout> cellLayouts = new ArrayList<>(getPanelCount() + 1);
-        cellLayouts.add(getHotseat());
-        forEachVisiblePage(page -> cellLayouts.add((CellLayout) page));
-
-        // Order: Preferred item, App icons in hotseat/workspace, app in folder in hotseat/workspace
-        if (ADAPTIVE_ICON_WINDOW_ANIM.get()) {
-            return getFirstMatch(cellLayouts, preferredItem, preferredItemInFolder,
-                    packageAndUserAndApp, packageAndUserAndAppInFolder);
-        } else {
-            // Do not use Folder as a criteria, since it'll cause a crash when trying to draw
-            // FolderAdaptiveIcon as the background.
-            return getFirstMatch(cellLayouts, preferredItem, packageAndUserAndApp);
-        }
-    }
-
     public View getHomescreenIconByItemId(final int id) {
         return getFirstMatch((info, v) -> info != null && info.id == id);
     }
@@ -3227,23 +3169,6 @@
         return value[0];
     }
 
-    /**
-     * Finds the first view matching the ordered operators across the given cell layouts by order.
-     * @param cellLayouts List of CellLayouts to scan, in order of preference.
-     * @param operators List of operators, in order starting from best matching operator.
-     */
-    View getFirstMatch(Iterable<CellLayout> cellLayouts, final ItemOperator... operators) {
-        for (ItemOperator operator : operators) {
-            for (CellLayout cellLayout : cellLayouts) {
-                View match = mapOverCellLayout(cellLayout, operator);
-                if (match != null) {
-                    return match;
-                }
-            }
-        }
-        return null;
-    }
-
     void clearDropTargets() {
         mapOverItems(new ItemOperator() {
             @Override
diff --git a/src/com/android/launcher3/anim/PropertySetter.java b/src/com/android/launcher3/anim/PropertySetter.java
index 729523f..8d77b4b 100644
--- a/src/com/android/launcher3/anim/PropertySetter.java
+++ b/src/com/android/launcher3/anim/PropertySetter.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.anim;
 
+import android.animation.Animator;
 import android.animation.TimeInterpolator;
 import android.util.FloatProperty;
 import android.util.IntProperty;
@@ -64,4 +65,9 @@
             TimeInterpolator interpolator) {
         property.setValue(target, value);
     }
+
+    default void add(Animator animatorSet) {
+        animatorSet.setDuration(0);
+        animatorSet.start();
+    }
 }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 98cb5ae..2d31aa4 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -92,9 +92,6 @@
     public static final BooleanFlag ENABLE_DEVICE_SEARCH = new DeviceFlag(
             "ENABLE_DEVICE_SEARCH", true, "Allows on device search in all apps");
 
-    public static final BooleanFlag ENABLE_ONE_SEARCH = new DeviceFlag("ENABLE_ONE_SEARCH", false,
-            "Use homescreen search box to complete allApps searches");
-
     public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(
             "ENABLE_TWOLINE_ALLAPPS", false, "Enables two line label inside all apps.");
 
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 0fc4c2d..01ebc53 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.InvariantDeviceProfile.DeviceType;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_2;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_3;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_4;
@@ -25,15 +28,11 @@
 import android.content.SharedPreferences;
 import android.text.TextUtils;
 
-import androidx.annotation.IntDef;
-
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
 import com.android.launcher3.util.IntSet;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.Locale;
 import java.util.Objects;
 
@@ -46,13 +45,6 @@
     public static final String KEY_HOTSEAT_COUNT = "migration_src_hotseat_count";
     public static final String KEY_DEVICE_TYPE = "migration_src_device_type";
 
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({TYPE_PHONE, TYPE_MULTI_DISPLAY, TYPE_TABLET})
-    public @interface DeviceType{}
-    public static final int TYPE_PHONE = 0;
-    public static final int TYPE_MULTI_DISPLAY = 1;
-    public static final int TYPE_TABLET = 2;
-
     private static final IntSet COMPATIBLE_TYPES = IntSet.wrap(TYPE_PHONE, TYPE_MULTI_DISPLAY);
 
     public static boolean deviceTypeCompatible(@DeviceType int typeA, @DeviceType int typeB) {
@@ -67,11 +59,7 @@
     public DeviceGridState(InvariantDeviceProfile idp) {
         mGridSizeString = String.format(Locale.ENGLISH, "%d,%d", idp.numColumns, idp.numRows);
         mNumHotseat = idp.numDatabaseHotseatIcons;
-        mDeviceType = idp.supportedProfiles.size() > 2
-                ? TYPE_MULTI_DISPLAY
-                : idp.supportedProfiles.stream().allMatch(dp -> dp.isTablet)
-                        ? TYPE_TABLET
-                        : TYPE_PHONE;
+        mDeviceType = idp.deviceType;
     }
 
     public DeviceGridState(Context context) {
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index b72f462..e30829e 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -25,6 +25,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SETTINGS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WALLPAPERS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
 import static com.android.launcher3.LauncherSettings.Favorites.EXTENDED_CONTAINERS;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
@@ -55,6 +56,7 @@
 import com.android.launcher3.logger.LauncherAtom.Shortcut;
 import com.android.launcher3.logger.LauncherAtom.ShortcutsContainer;
 import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer;
+import com.android.launcher3.logger.LauncherAtom.WallpapersContainer;
 import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.util.ContentWriter;
@@ -428,6 +430,10 @@
                 return ContainerInfo.newBuilder()
                         .setTaskSwitcherContainer(TaskSwitcherContainer.getDefaultInstance())
                         .build();
+            case CONTAINER_WALLPAPERS:
+                return ContainerInfo.newBuilder()
+                        .setWallpapersContainer(WallpapersContainer.getDefaultInstance())
+                        .build();
             case EXTENDED_CONTAINERS:
                 return ContainerInfo.newBuilder()
                         .setExtendedContainers(getExtendedContainer())
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 8f607a1..4c0930b 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -16,8 +16,8 @@
 
 package com.android.launcher3.provider;
 
-import static com.android.launcher3.model.DeviceGridState.TYPE_MULTI_DISPLAY;
-import static com.android.launcher3.model.DeviceGridState.TYPE_PHONE;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 
 import android.app.backup.BackupManager;
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 5fe5450..d52594e 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -90,9 +90,4 @@
     public float getWorkspaceBackgroundAlpha(Launcher launcher) {
         return 0.2f;
     }
-
-    @Override
-    public int getVisibleElements(Launcher launcher) {
-        return (super.getVisibleElements(launcher) | HOTSEAT_ICONS) & ~TASKBAR;
-    }
 }
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 1276ece..52c3581 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -17,6 +17,7 @@
 
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
+import android.content.Context;
 import android.graphics.PointF;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -26,6 +27,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.R;
+
 import java.util.LinkedList;
 import java.util.Queue;
 
@@ -44,10 +47,9 @@
     private static final boolean DBG = false;
     private static final String TAG = "BaseSwipeDetector";
     private static final float ANIMATION_DURATION = 1200;
-    /** The minimum release velocity in pixels per millisecond that triggers fling.*/
-    private static final float RELEASE_VELOCITY_PX_MS = 1.0f;
     private static final PointF sTempPoint = new PointF();
 
+    private final float mReleaseVelocity;
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     protected final boolean mIsRtl;
@@ -64,6 +66,7 @@
     private boolean mIsSettingState;
 
     protected boolean mIgnoreSlopWhenSettling;
+    protected Context mContext;
 
     private enum ScrollState {
         IDLE,
@@ -71,10 +74,14 @@
         SETTLING       // onDragEnd
     }
 
-    protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) {
+    protected BaseSwipeDetector(@NonNull Context context, @NonNull ViewConfiguration config,
+            boolean isRtl) {
         mTouchSlop = config.getScaledTouchSlop();
         mMaxVelocity = config.getScaledMaximumFlingVelocity();
         mIsRtl = isRtl;
+        mContext = context;
+        mReleaseVelocity = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.base_swift_detector_fling_release_velocity);
     }
 
     public static long calculateDuration(float velocity, float progressNeeded) {
@@ -120,7 +127,7 @@
     }
 
     public boolean isFling(float velocity) {
-        return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS;
+        return Math.abs(velocity) > mReleaseVelocity;
     }
 
     public boolean onTouchEvent(MotionEvent ev) {
@@ -236,7 +243,7 @@
         } else {
             mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
             mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
-        } 
+        }
     }
 
     protected abstract boolean shouldScrollStart(PointF displacement);
diff --git a/src/com/android/launcher3/touch/BothAxesSwipeDetector.java b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
index 944391e..6e2f0d8 100644
--- a/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
@@ -21,7 +21,6 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
 
@@ -43,13 +42,7 @@
     private int mScrollDirections;
 
     public BothAxesSwipeDetector(@NonNull Context context, @NonNull Listener l) {
-        this(ViewConfiguration.get(context), l, Utilities.isRtl(context.getResources()));
-    }
-
-    @VisibleForTesting
-    protected BothAxesSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
-            boolean isRtl) {
-        super(config, isRtl);
+        super(context, ViewConfiguration.get(context), Utilities.isRtl(context.getResources()));
         mListener = l;
     }
 
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index c255225..9a2d6d8 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -255,7 +255,7 @@
     }
 
     @Override
-    public int getSplitTranslationDirectionFactor(int stagePosition) {
+    public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
         if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
             return -1;
         } else {
@@ -357,7 +357,7 @@
     public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
         // Add "left" side of phone which is actually the top
         return Collections.singletonList(new SplitPositionOption(
-                R.drawable.ic_split_screen, R.string.split_screen_position_left,
+                R.drawable.ic_split_left, R.string.split_screen_position_left,
                 STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
     }
 
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 54b30fb..95168fb 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -95,7 +95,8 @@
     int getScrollOffsetStart(View view, Rect insets);
     int getScrollOffsetEnd(View view, Rect insets);
     int getSecondaryTranslationDirectionFactor();
-    int getSplitTranslationDirectionFactor(@StagePosition int stagePosition);
+    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);
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index e69944a..7d70f77 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -261,8 +261,8 @@
     }
 
     @Override
-    public int getSplitTranslationDirectionFactor(int stagePosition) {
-        if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
+    public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
+        if (deviceProfile.isLandscape && stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
             return -1;
         } else {
             return 1;
@@ -370,28 +370,27 @@
     public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
         List<SplitPositionOption> options = new ArrayList<>(1);
         // Add both left and right options if we're in tablet mode
-        // TODO: Add in correct icons
         if (dp.isTablet && dp.isLandscape) {
             options.add(new SplitPositionOption(
-                    R.drawable.ic_split_screen, R.string.split_screen_position_right,
+                    R.drawable.ic_split_right, R.string.split_screen_position_right,
                     STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
             options.add(new SplitPositionOption(
-                    R.drawable.ic_split_screen, R.string.split_screen_position_left,
+                    R.drawable.ic_split_left, R.string.split_screen_position_left,
                     STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
         } else {
             if (dp.isSeascape()) {
                 // Add left/right options
                 options.add(new SplitPositionOption(
-                        R.drawable.ic_split_screen, R.string.split_screen_position_right,
+                        R.drawable.ic_split_right, R.string.split_screen_position_right,
                         STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
             } else if (dp.isLandscape) {
                 options.add(new SplitPositionOption(
-                        R.drawable.ic_split_screen, R.string.split_screen_position_left,
+                        R.drawable.ic_split_left, R.string.split_screen_position_left,
                         STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
             } else {
                 // Only add top option
                 options.add(new SplitPositionOption(
-                        R.drawable.ic_split_screen, R.string.split_screen_position_top,
+                        R.drawable.ic_split_top, R.string.split_screen_position_top,
                         STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
             }
         }
@@ -542,17 +541,18 @@
 
         if (deviceProfile.isLandscape) {
             primaryIconParams.gravity = TOP | START;
-            primaryIconView.setTranslationX(primarySnapshotWidth - primaryIconView.getWidth());
+            primaryIconView.setTranslationX(
+                    primarySnapshotWidth - primaryIconView.getMeasuredWidth());
             primaryIconView.setTranslationY(0);
             secondaryIconParams.gravity = TOP | START;
             secondaryIconView.setTranslationX(primarySnapshotWidth + dividerBar);
         } else {
             primaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
-            primaryIconView.setTranslationX(-(primaryIconView.getWidth()) / 2f);
+            primaryIconView.setTranslationX(-(primaryIconView.getMeasuredWidth()) / 2f);
             primaryIconView.setTranslationY(0);
 
             secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
-            secondaryIconView.setTranslationX(secondaryIconView.getWidth() / 2f);
+            secondaryIconView.setTranslationX(secondaryIconView.getMeasuredWidth() / 2f);
         }
         secondaryIconView.setTranslationY(0);
         primaryIconView.setLayoutParams(primaryIconParams);
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index 539e3f8..a921f94 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -37,7 +37,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
 import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds;
 import com.android.launcher3.views.BaseDragLayer;
@@ -53,7 +52,7 @@
     }
 
     @Override
-    public int getSplitTranslationDirectionFactor(int stagePosition) {
+    public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
         if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
             return -1;
         } else {
@@ -115,7 +114,7 @@
     public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
         // Add "right" option which is actually the top
         return Collections.singletonList(new SplitPositionOption(
-                R.drawable.ic_split_screen, R.string.split_screen_position_right,
+                R.drawable.ic_split_right, R.string.split_screen_position_right,
                 STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
     }
 
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index f751b7d..5c599c0 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -106,13 +106,15 @@
 
     public SingleAxisSwipeDetector(@NonNull Context context, @NonNull Listener l,
             @NonNull Direction dir) {
-        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
+        super(context, ViewConfiguration.get(context),  Utilities.isRtl(context.getResources()));
+        mListener = l;
+        mDir = dir;
     }
 
     @VisibleForTesting
-    protected SingleAxisSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
-            @NonNull Direction dir, boolean isRtl) {
-        super(config, isRtl);
+    protected SingleAxisSwipeDetector(@NonNull Context context, @NonNull ViewConfiguration config,
+            @NonNull Listener l, @NonNull Direction dir, boolean isRtl) {
+        super(context, config, isRtl);
         mListener = l;
         mDir = dir;
     }
diff --git a/src/com/android/launcher3/util/ActivityLifecycleCallbacksAdapter.java b/src/com/android/launcher3/util/ActivityLifecycleCallbacksAdapter.java
new file mode 100644
index 0000000..baa8418
--- /dev/null
+++ b/src/com/android/launcher3/util/ActivityLifecycleCallbacksAdapter.java
@@ -0,0 +1,29 @@
+package com.android.launcher3.util;
+
+import android.app.Activity;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.os.Bundle;
+
+public interface ActivityLifecycleCallbacksAdapter extends ActivityLifecycleCallbacks {
+
+    default void onActivityCreated(Activity activity, Bundle bundle) {
+    }
+
+    default void onActivityDestroyed(Activity activity) {
+    }
+
+    default void onActivityPaused(Activity activity) {
+    }
+
+    default void onActivityResumed(Activity activity) {
+    }
+
+    default void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
+    }
+
+    default void onActivityStarted(Activity activity) {
+    }
+
+    default void onActivityStopped(Activity activity) {
+    }
+}
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index ab3083d..7917410 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -20,6 +20,7 @@
 import android.os.UserHandle;
 
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
 
@@ -85,8 +86,16 @@
     }
 
     static ItemInfoMatcher ofShortcutKeys(Set<ShortcutKey> keys) {
-        return  (info, cn) -> info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
-                        keys.contains(ShortcutKey.fromItemInfo(info));
+        return (info, cn) -> info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                && keys.contains(ShortcutKey.fromItemInfo(info));
+    }
+
+    /**
+     * Returns a matcher for items within folders.
+     */
+    static ItemInfoMatcher forFolderMatch(ItemInfoMatcher childOperator) {
+        return (info, cn) -> info instanceof FolderInfo && ((FolderInfo) info).contents.stream()
+                .anyMatch(childOperator::matchesInfo);
     }
 
     /**
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 8b7ad46..27cf134 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -460,7 +460,7 @@
         if (mIconLoadResult != null && mIconLoadResult.isIconLoaded) {
             setVisibility(View.VISIBLE);
         }
-        if (!mIsOpening) {
+        if (!mIsOpening && mOriginalIcon != null) {
             // When closing an app, we want the item on the workspace to be invisible immediately
             setIconAndDotVisible(mOriginalIcon, false);
         }
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
index e2e3be7..f32f904 100644
--- a/src/com/android/launcher3/views/FloatingSurfaceView.java
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -158,7 +158,7 @@
         if (mContract == null) {
             return;
         }
-        View icon = mLauncher.getWorkspace().getFirstMatchForAppClose(-1,
+        View icon = mLauncher.getFirstMatchForAppClose(-1,
                 mContract.componentName.getPackageName(), mContract.user);
 
         boolean iconChanged = mIcon != icon;
@@ -182,7 +182,7 @@
                 lp.topMargin = Math.round(mIconPosition.top);
             }
         }
-        if (iconChanged && !mIconBounds.isEmpty()) {
+        if (mIcon != null && iconChanged && !mIconBounds.isEmpty()) {
             // Record the icon display
             setCurrentIconVisible(true);
             Canvas c = mPicture.beginRecording(mIconBounds.width(), mIconBounds.height());
diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java
index 49fcd2e..e582114 100644
--- a/src/com/android/launcher3/views/Snackbar.java
+++ b/src/com/android/launcher3/views/Snackbar.java
@@ -28,8 +28,9 @@
 import android.view.MotionEvent;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -44,7 +45,7 @@
     private static final long HIDE_DURATION_MS = 180;
     private static final int TIMEOUT_DURATION_MS = 4000;
 
-    private final BaseDraggingActivity mActivity;
+    private final ActivityContext mActivity;
     private Runnable mOnDismissed;
 
     public Snackbar(Context context, AttributeSet attrs) {
@@ -53,12 +54,19 @@
 
     public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mActivity = BaseDraggingActivity.fromContext(context);
+        mActivity = ActivityContext.lookupContext(context);
         inflate(context, R.layout.snackbar, this);
     }
 
-    public static void show(BaseDraggingActivity activity, int labelStringResId,
-            int actionStringResId, Runnable onDismissed, Runnable onActionClicked) {
+    /** Show a snackbar with just a label. */
+    public static <T extends Context & ActivityContext> void show(T activity, int labelStringRedId,
+            Runnable onDismissed) {
+        show(activity, labelStringRedId, NO_ID, onDismissed, null);
+    }
+
+    /** Show a snackbar with a label and action. */
+    public static <T extends Context & ActivityContext> void show(T activity, int labelStringResId,
+            int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked) {
         closeOpenViews(activity, true, TYPE_SNACKBAR);
         Snackbar snackbar = new Snackbar(activity, null);
         // Set some properties here since inflated xml only contains the children.
@@ -80,20 +88,42 @@
         int maxMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_max_margin_left_right);
         int minMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_min_margin_left_right);
         int marginBottom = res.getDimensionPixelSize(R.dimen.snackbar_margin_bottom);
+        int absoluteMaxWidth = res.getDimensionPixelSize(R.dimen.snackbar_max_width);
         Rect insets = activity.getDeviceProfile().getInsets();
-        int maxWidth = dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right;
-        int minWidth = dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right;
+        int maxWidth = Math.min(
+                dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right,
+                absoluteMaxWidth);
+        int minWidth = Math.min(
+                dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right,
+                absoluteMaxWidth);
         params.width = minWidth;
         params.setMargins(0, 0, 0, marginBottom + insets.bottom);
 
         TextView labelView = snackbar.findViewById(R.id.label);
-        TextView actionView = snackbar.findViewById(R.id.action);
         String labelText = res.getString(labelStringResId);
-        String actionText = res.getString(actionStringResId);
-        int totalContentWidth = (int) (labelView.getPaint().measureText(labelText)
-                + actionView.getPaint().measureText(actionText))
+        labelView.setText(labelText);
+
+        TextView actionView = snackbar.findViewById(R.id.action);
+        float actionWidth;
+        if (actionStringResId != NO_ID) {
+            String actionText = res.getString(actionStringResId);
+            actionWidth = actionView.getPaint().measureText(actionText)
+                    + actionView.getPaddingRight() + actionView.getPaddingLeft();
+            actionView.setText(actionText);
+            actionView.setOnClickListener(v -> {
+                if (onActionClicked != null) {
+                    onActionClicked.run();
+                }
+                snackbar.mOnDismissed = null;
+                snackbar.close(true);
+            });
+        } else {
+            actionWidth = 0;
+            actionView.setVisibility(GONE);
+        }
+
+        int totalContentWidth = (int) (labelView.getPaint().measureText(labelText) + actionWidth)
                 + labelView.getPaddingRight() + labelView.getPaddingLeft()
-                + actionView.getPaddingRight() + actionView.getPaddingLeft()
                 + padding * 2;
         if (totalContentWidth > params.width) {
             // The text doesn't fit in our standard width so update width to accommodate.
@@ -113,17 +143,8 @@
                 params.width = maxWidth;
             }
         }
-        labelView.setText(labelText);
-        actionView.setText(actionText);
-        actionView.setOnClickListener(v -> {
-            if (onActionClicked != null) {
-                onActionClicked.run();
-            }
-            snackbar.mOnDismissed = null;
-            snackbar.close(true);
-        });
-        snackbar.mOnDismissed = onDismissed;
 
+        snackbar.mOnDismissed = onDismissed;
         snackbar.setAlpha(0);
         snackbar.setScaleX(0.8f);
         snackbar.setScaleY(0.8f);
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 4390211..618ab41 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -250,5 +250,21 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity-alias android:name="Activity13" android:exported="true"
+            android:label="TestActivity13"
+            android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="Activity14" android:exported="true"
+            android:label="TestActivity14"
+            android:targetActivity="com.android.launcher3.testcomponent.OtherBaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
     </application>
 </manifest>
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 472e1a1..260f556 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -30,6 +30,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -58,6 +59,7 @@
     private TouchEventGenerator mGenerator;
     private SingleAxisSwipeDetector mDetector;
     private int mTouchSlop;
+    Context mContext;
 
     @Mock
     private SingleAxisSwipeDetector.Listener mMockListener;
@@ -69,12 +71,13 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mGenerator = new TouchEventGenerator((ev) -> mDetector.onTouchEvent(ev));
-        ViewConfiguration orgConfig = ViewConfiguration
-                .get(InstrumentationRegistry.getTargetContext());
+        mContext = InstrumentationRegistry.getTargetContext();
+        ViewConfiguration orgConfig = ViewConfiguration.get(mContext);
         doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig)
                 .getScaledMaximumFlingVelocity();
 
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, VERTICAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_BOTH, false);
         mTouchSlop = orgConfig.getScaledTouchSlop();
         doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
@@ -84,7 +87,8 @@
 
     @Test
     public void testDragStart_verticalPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, VERTICAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 - mTouchSlop);
@@ -94,7 +98,8 @@
 
     @Test
     public void testDragStart_verticalNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, VERTICAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
@@ -112,7 +117,8 @@
 
     @Test
     public void testDragStart_horizontalPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
@@ -123,7 +129,8 @@
 
     @Test
     public void testDragStart_horizontalNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, false);
         mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
@@ -134,7 +141,8 @@
 
     @Test
     public void testDragStart_horizontalRtlPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, true);
         mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
@@ -145,7 +153,8 @@
 
     @Test
     public void testDragStart_horizontalRtlNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector = new SingleAxisSwipeDetector(mContext,
+                mMockConfig, mMockListener, HORIZONTAL, true);
         mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 19dca45..075505e 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -149,7 +149,7 @@
                 }
                 sDumpWasGenerated = true;
                 Log.d("b/195319692", "sDumpWasGenerated := true", new Exception());
-                result = "memory dump filename: " + fileName;
+                result = "saved memory dump as an artifact";
             } catch (Throwable e) {
                 Log.e(TAG, "dumpHprofData failed", e);
                 result = "failed to save memory dump";
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index c90d283..41c7c37 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -80,12 +80,8 @@
         assertTrue(message, failed);
     }
 
-    private int pagesPerScreen() {
-        return mLauncher.isTwoPanels() ? 2 : 1;
-    }
-
-    private boolean isWorkspaceScrollable(Launcher launcher) {
-        return launcher.getWorkspace().getPageCount() > pagesPerScreen();
+    public static boolean isWorkspaceScrollable(Launcher launcher) {
+        return launcher.getWorkspace().getPageCount() > launcher.getWorkspace().getPanelCount();
     }
 
     private int getCurrentWorkspacePage(Launcher launcher) {
@@ -192,7 +188,7 @@
         executeOnLauncher(
                 launcher -> assertEquals(
                         "Ensuring workspace scrollable didn't switch to next screen",
-                        pagesPerScreen(), getCurrentWorkspacePage(launcher)));
+                        workspace.pagesPerScreen(), getCurrentWorkspacePage(launcher)));
         executeOnLauncher(
                 launcher -> assertTrue("ensureScrollable didn't make workspace scrollable",
                         isWorkspaceScrollable(launcher)));
@@ -209,7 +205,7 @@
         workspace.flingForward();
         executeOnLauncher(
                 launcher -> assertEquals("Flinging forward didn't switch workspace to next screen",
-                        pagesPerScreen(), getCurrentWorkspacePage(launcher)));
+                        workspace.pagesPerScreen(), getCurrentWorkspacePage(launcher)));
         assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL));
 
         // Test starting a workspace app.
diff --git a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
new file mode 100644
index 0000000..b048cd4
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2021 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.ui.workspace;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.TaplTestsLauncher3;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Tests for two panel workspace.
+ *
+ * Note running these tests will clear the workspace on the device.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest {
+
+    Workspace mWorkspace;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        TaplTestsLauncher3.initialize(this);
+        mWorkspace = mLauncher.getWorkspace();
+    }
+
+    @Test
+    public void testDragIconToRightPanel() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Maps", "Play Store");
+            assertItemsOnPage(launcher, 1, "Chrome");
+        });
+    }
+
+    @Test
+    public void testDragIconToPage2() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Maps");
+            assertPageEmpty(launcher, 3);
+        });
+    }
+
+    @Test
+    public void testDragIconToPage3() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Phone"), 3);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+            assertPageEmpty(launcher, 2);
+            assertItemsOnPage(launcher, 3, "Phone");
+        });
+    }
+
+
+    @Test
+    public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 3);
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Chrome"), 0);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Chrome");
+            assertItemsOnPage(launcher, 3, "Maps");
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), -1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertItemsOnPage(launcher, 1, "Maps");
+            assertItemsOnPage(launcher, 2, "Chrome");
+            assertPageEmpty(launcher, 3);
+        });
+
+        // Move Chrome to the right panel as well, to make sure pages are not deleted whichever
+        // page is the empty one
+        mWorkspace.flingForward();
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Chrome"), 1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertItemsOnPage(launcher, 1, "Maps");
+            assertPageEmpty(launcher, 2);
+            assertItemsOnPage(launcher, 3, "Chrome");
+        });
+    }
+
+
+    @Test
+    public void testEmptyPagesGetRemovedIfBothPagesAreEmpty() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), 2);
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Camera"), 1);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3);
+            assertItemsOnPage(launcher, 0, "Maps");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Play Store");
+            assertItemsOnPage(launcher, 3, "Camera");
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Camera"), -1);
+        mWorkspace.flingForward();
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Play Store"), -2);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertItemsOnPage(launcher, 1, "Camera");
+        });
+    }
+
+    @Test
+    public void testMiddleEmptyPagesGetRemoved() {
+        if (!mLauncher.isTwoPanels()) {
+            return;
+        }
+
+        // Pre verifying the screens
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1);
+            assertItemsOnPage(launcher, 0, "Play Store", "Maps");
+            assertPageEmpty(launcher, 1);
+        });
+
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2);
+        mWorkspace.dragIcon(mWorkspace.getHotseatAppIcon("Messages"), 3);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 2, 3, 4, 5);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 2, "Maps");
+            assertPageEmpty(launcher, 3);
+            assertPageEmpty(launcher, 4);
+            assertItemsOnPage(launcher, 5, "Messages");
+        });
+
+        mWorkspace.flingBackward();
+        mWorkspace.dragIcon(mWorkspace.getWorkspaceAppIcon("Maps"), 2);
+
+        executeOnLauncher(launcher -> {
+            assertPagesExist(launcher, 0, 1, 4, 5);
+            assertItemsOnPage(launcher, 0, "Play Store");
+            assertPageEmpty(launcher, 1);
+            assertItemsOnPage(launcher, 4, "Maps");
+            assertItemsOnPage(launcher, 5, "Messages");
+        });
+    }
+
+    private void assertPageEmpty(Launcher launcher, int pageId) {
+        CellLayout page = launcher.getWorkspace().getScreenWithId(pageId);
+        assertNotNull("Page " + pageId + " does NOT exist.", page);
+        assertEquals("Page " + pageId + " is NOT empty. Number of items on the page:", 0,
+                page.getShortcutsAndWidgets().getChildCount());
+    }
+
+    private void assertPagesExist(Launcher launcher, int... pageIds) {
+        int pageCount = launcher.getWorkspace().getPageCount();
+        assertEquals("Existing page count does NOT match.", pageIds.length, pageCount);
+        for (int i = 0; i < pageCount; i++) {
+            CellLayout page = (CellLayout) launcher.getWorkspace().getPageAt(i);
+            int pageId = launcher.getWorkspace().getIdForScreen(page);
+            assertEquals("The page's id at index " + i + " does NOT match.", pageId,
+                    pageIds[i]);
+        }
+    }
+
+    private void assertItemsOnPage(Launcher launcher, int pageId, String... itemTitles) {
+        Set<String> itemTitleSet = Arrays.stream(itemTitles).collect(Collectors.toSet());
+        CellLayout page = launcher.getWorkspace().getScreenWithId(pageId);
+        int itemCount = page.getShortcutsAndWidgets().getChildCount();
+        for (int i = 0; i < itemCount; i++) {
+            ItemInfo itemInfo = (ItemInfo) page.getShortcutsAndWidgets().getChildAt(i).getTag();
+            if (itemInfo != null) {
+                assertTrue("There was an extra item on page " + pageId + ": " + itemInfo.title,
+                        itemTitleSet.remove(itemInfo.title));
+            }
+        }
+        assertTrue("Could NOT find some of the items on page " + pageId + ": "
+                        + itemTitleSet.stream().collect(Collectors.joining(",")),
+                itemTitleSet.isEmpty());
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 6e7264a..0bac2ca 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -77,7 +77,8 @@
             mLauncher.scroll(
                     widgetsContainer,
                     Direction.UP,
-                    new Rect(0, 0, mLauncher.getVisibleBounds(widgetsContainer).width(), 0),
+                    new Rect(0, 0, mLauncher.getRightGestureMarginInContainer(widgetsContainer) + 1,
+                            0),
                     FLING_STEPS, false);
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("flung back")) {
                 verifyActiveContainer();
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 0145690..d9f5cc8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -145,16 +145,7 @@
             if (!isWorkspaceScrollable(workspace)) {
                 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                         "dragging icon to a second page of workspace to make it scrollable")) {
-                    dragIconToWorkspace(
-                            mLauncher,
-                            getHotseatAppIcon("Chrome"),
-                            new Point(mLauncher.getDevice().getDisplayWidth(),
-                                    mLauncher.getVisibleBounds(workspace).centerY()),
-                            "popup_container",
-                            false,
-                            false,
-                            () -> mLauncher.expectEvent(
-                                    TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
+                    dragIcon(workspace, getHotseatAppIcon("Chrome"), pagesPerScreen());
                     verifyActiveContainer();
                 }
             }
@@ -163,6 +154,48 @@
         }
     }
 
+    /**
+     * Returns the number of pages that are visible on the screen simultaneously.
+     */
+    public int pagesPerScreen() {
+        return mLauncher.isTwoPanels() ? 2 : 1;
+    }
+
+    /**
+     * Drags an icon to the (currentPage + pageDelta) page if the page already exists.
+     * If the target page doesn't exist, the icon will be put onto an existing page that is the
+     * closest to the target page.
+     *
+     * @param appIcon   - icon to drag.
+     * @param pageDelta - how many pages should the icon be dragged from the current page.
+     *                    It can be a negative value.
+     */
+    public void dragIcon(AppIcon appIcon, int pageDelta) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final UiObject2 workspace = verifyActiveContainer();
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "dragging icon to page with delta: " + pageDelta)) {
+                dragIcon(workspace, appIcon, pageDelta);
+                verifyActiveContainer();
+            }
+        }
+    }
+
+    private void dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta) {
+        int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen();
+        int targetX = (pageWidth / 2) + pageWidth * pageDelta;
+        dragIconToWorkspace(
+                mLauncher,
+                appIcon,
+                new Point(targetX, mLauncher.getVisibleBounds(workspace).centerY()),
+                "popup_container",
+                false,
+                false,
+                () -> mLauncher.expectEvent(
+                        TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
+        verifyActiveContainer();
+    }
+
     private boolean isWorkspaceScrollable(UiObject2 workspace) {
         return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1);
     }
@@ -258,10 +291,26 @@
         try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer(
                 "want to drag icon to workspace")) {
             final long downTime = SystemClock.uptimeMillis();
-            final Point dragStartCenter = dragIconToSpringLoaded(launcher, downTime,
+            Point dragStart = dragIconToSpringLoaded(launcher, downTime,
                     launchable.getObject(), longPressIndicator, expectLongClickEvents);
-            final Point targetDest = dest.get();
-            launcher.movePointer(dragStartCenter, targetDest, 10, downTime, true,
+            Point targetDest = dest.get();
+            int displayX = launcher.getRealDisplaySize().x;
+
+            // Since the destination can be on another page, we need to drag to the edge first
+            // until we reach the target page
+            while (targetDest.x > displayX || targetDest.x < 0) {
+                int edgeX = targetDest.x > 0 ? displayX : 0;
+                Point screenEdge = new Point(edgeX, targetDest.y);
+                launcher.movePointer(dragStart, screenEdge, 10, downTime, true,
+                        LauncherInstrumentation.GestureScope.INSIDE);
+                launcher.waitForIdle(); // Wait for the page change to happen
+                targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1);
+                dragStart = screenEdge;
+            }
+
+            // targetDest.x is now between 0 and displayX so we found the target page,
+            // we just have to put move the icon to the destination and drop it
+            launcher.movePointer(dragStart, targetDest, 10, downTime, true,
                     LauncherInstrumentation.GestureScope.INSIDE);
             dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
         }