Merge "Syncing Launcher3 atest XML config with the GCL one" into main
diff --git a/go/quickstep/res/layout/overview_actions_container.xml b/go/quickstep/res/layout/overview_actions_container.xml
index 48650aa..077cfae 100644
--- a/go/quickstep/res/layout/overview_actions_container.xml
+++ b/go/quickstep/res/layout/overview_actions_container.xml
@@ -120,6 +120,16 @@
             android:layout_height="1dp"
             android:layout_weight="1"
             android:visibility="gone" />
+
+        <Button
+            android:id="@+id/action_save_app_pair"
+            style="@style/GoOverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableStart="@drawable/ic_save_app_pair"
+            android:text="@string/action_save_app_pair"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor"
+            android:visibility="gone" />
     </LinearLayout>
 
 </com.android.quickstep.views.GoOverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index 0fda0bf..5bd5823 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -55,6 +55,15 @@
             android:theme="@style/ThemeControlHighlightWorkspaceColor"
             android:visibility="gone" />
 
+        <Button
+            android:id="@+id/action_save_app_pair"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/action_save_app_pair"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor"
+            android:visibility="gone" />
+
         <Space
             android:layout_width="0dp"
             android:layout_height="1dp"
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 14f615e..75a4fc8 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -230,6 +230,8 @@
     <string name="action_screenshot">Screenshot</string>
     <!-- Label for a button that enters split screen selection mode. [CHAR_LIMIT=20] -->
     <string name="action_split">Split</string>
+    <!-- Label for a button that saves a new app pair. [CHAR_LIMIT=20] -->
+    <string name="action_save_app_pair">Save app pair</string>
     <!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
     <string name="toast_split_select_app">Tap another app to use split screen</string>
     <string name="toast_contextual_split_select_app">Choose another app to use split screen</string>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 75b8796c..66e20d7 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -64,6 +64,7 @@
 import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
+import static com.android.quickstep.util.AnimUtils.clampToDuration;
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
@@ -748,34 +749,35 @@
         final float finalShadowRadius = appTargetsAreTranslucent ? 0 : mMaxShadowRadius;
 
         MultiValueUpdateListener listener = new MultiValueUpdateListener() {
-            FloatProp mDx = new FloatProp(0, prop.dX, 0, APP_LAUNCH_DURATION,
-                    mOpeningXInterpolator);
-            FloatProp mDy = new FloatProp(0, prop.dY, 0, APP_LAUNCH_DURATION,
-                    mOpeningInterpolator);
+            FloatProp mDx = new FloatProp(0, prop.dX, mOpeningXInterpolator);
+            FloatProp mDy = new FloatProp(0, prop.dY, mOpeningInterpolator);
 
             FloatProp mIconScaleToFitScreen = new FloatProp(prop.initialAppIconScale,
-                    prop.finalAppIconScale, 0, APP_LAUNCH_DURATION, mOpeningInterpolator);
+                    prop.finalAppIconScale, mOpeningInterpolator);
             FloatProp mIconAlpha = new FloatProp(prop.iconAlphaStart, 0f,
-                    APP_LAUNCH_ALPHA_START_DELAY, APP_LAUNCH_ALPHA_DURATION, LINEAR);
+                    clampToDuration(LINEAR, APP_LAUNCH_ALPHA_START_DELAY, APP_LAUNCH_ALPHA_DURATION,
+                            APP_LAUNCH_DURATION));
 
-            FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius, 0,
-                    APP_LAUNCH_DURATION, mOpeningInterpolator);
-            FloatProp mShadowRadius = new FloatProp(0, finalShadowRadius, 0,
-                    APP_LAUNCH_DURATION, mOpeningInterpolator);
+            FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius,
+                    mOpeningInterpolator);
+            FloatProp mShadowRadius = new FloatProp(0, finalShadowRadius,
+                    mOpeningInterpolator);
 
             FloatProp mCropRectCenterX = new FloatProp(prop.cropCenterXStart, prop.cropCenterXEnd,
-                    0, APP_LAUNCH_DURATION, mOpeningInterpolator);
+                    mOpeningInterpolator);
             FloatProp mCropRectCenterY = new FloatProp(prop.cropCenterYStart, prop.cropCenterYEnd,
-                    0, APP_LAUNCH_DURATION, mOpeningInterpolator);
-            FloatProp mCropRectWidth = new FloatProp(prop.cropWidthStart, prop.cropWidthEnd, 0,
-                    APP_LAUNCH_DURATION, mOpeningInterpolator);
-            FloatProp mCropRectHeight = new FloatProp(prop.cropHeightStart, prop.cropHeightEnd, 0,
-                    APP_LAUNCH_DURATION, mOpeningInterpolator);
+                    mOpeningInterpolator);
+            FloatProp mCropRectWidth = new FloatProp(prop.cropWidthStart, prop.cropWidthEnd,
+                    mOpeningInterpolator);
+            FloatProp mCropRectHeight = new FloatProp(prop.cropHeightStart, prop.cropHeightEnd,
+                    mOpeningInterpolator);
 
-            FloatProp mNavFadeOut = new FloatProp(1f, 0f, 0, ANIMATION_NAV_FADE_OUT_DURATION,
-                    NAV_FADE_OUT_INTERPOLATOR);
-            FloatProp mNavFadeIn = new FloatProp(0f, 1f, ANIMATION_DELAY_NAV_FADE_IN,
-                    ANIMATION_NAV_FADE_IN_DURATION, NAV_FADE_IN_INTERPOLATOR);
+            FloatProp mNavFadeOut = new FloatProp(1f, 0f, clampToDuration(
+                    NAV_FADE_OUT_INTERPOLATOR, 0, ANIMATION_NAV_FADE_OUT_DURATION,
+                    APP_LAUNCH_DURATION));
+            FloatProp mNavFadeIn = new FloatProp(0f, 1f, clampToDuration(
+                    NAV_FADE_IN_INTERPOLATOR, ANIMATION_DELAY_NAV_FADE_IN,
+                    ANIMATION_NAV_FADE_IN_DURATION, APP_LAUNCH_DURATION));
 
             @Override
             public void onUpdate(float percent, boolean initOnly) {
@@ -968,37 +970,36 @@
 
         appAnimator.addUpdateListener(new MultiValueUpdateListener() {
             float mAppWindowScale = 1;
-            final FloatProp mWidgetForegroundAlpha = new FloatProp(1 /* start */,
-                    0 /* end */, 0 /* delay */,
-                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
-            final FloatProp mWidgetFallbackBackgroundAlpha = new FloatProp(0 /* start */,
-                    1 /* end */, 0 /* delay */, 75 /* duration */, LINEAR);
-            final FloatProp mPreviewAlpha = new FloatProp(0 /* start */, 1 /* end */,
+            final FloatProp mWidgetForegroundAlpha = new FloatProp(1, 0, clampToDuration(
+                    LINEAR, 0, WIDGET_CROSSFADE_DURATION_MILLIS / 2, APP_LAUNCH_DURATION));
+
+            final FloatProp mWidgetFallbackBackgroundAlpha = new FloatProp(0, 1,
+                    clampToDuration(LINEAR, 0, 75, APP_LAUNCH_DURATION));
+            final FloatProp mPreviewAlpha = new FloatProp(0, 1, clampToDuration(
+                    LINEAR,
                     WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* delay */,
-                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */,
+                    APP_LAUNCH_DURATION));
             final FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius,
-                    0 /* start */, APP_LAUNCH_DURATION, mOpeningInterpolator);
-            final FloatProp mCornerRadiusProgress = new FloatProp(0, 1, 0, APP_LAUNCH_DURATION,
                     mOpeningInterpolator);
+            final FloatProp mCornerRadiusProgress = new FloatProp(0, 1, mOpeningInterpolator);
 
             // Window & widget background positioning bounds
             final FloatProp mDx = new FloatProp(widgetBackgroundBounds.centerX(),
-                    windowTargetBounds.centerX(), 0 /* delay */, APP_LAUNCH_DURATION,
-                    mOpeningXInterpolator);
+                    windowTargetBounds.centerX(), mOpeningXInterpolator);
             final FloatProp mDy = new FloatProp(widgetBackgroundBounds.centerY(),
-                    windowTargetBounds.centerY(), 0 /* delay */, APP_LAUNCH_DURATION,
-                    mOpeningInterpolator);
+                    windowTargetBounds.centerY(), mOpeningInterpolator);
             final FloatProp mWidth = new FloatProp(widgetBackgroundBounds.width(),
-                    windowTargetBounds.width(), 0 /* delay */, APP_LAUNCH_DURATION,
-                    mOpeningInterpolator);
+                    windowTargetBounds.width(), mOpeningInterpolator);
             final FloatProp mHeight = new FloatProp(widgetBackgroundBounds.height(),
-                    windowTargetBounds.height(), 0 /* delay */, APP_LAUNCH_DURATION,
-                    mOpeningInterpolator);
+                    windowTargetBounds.height(), mOpeningInterpolator);
 
-            final FloatProp mNavFadeOut = new FloatProp(1f, 0f, 0, ANIMATION_NAV_FADE_OUT_DURATION,
-                    NAV_FADE_OUT_INTERPOLATOR);
-            final FloatProp mNavFadeIn = new FloatProp(0f, 1f, ANIMATION_DELAY_NAV_FADE_IN,
-                    ANIMATION_NAV_FADE_IN_DURATION, NAV_FADE_IN_INTERPOLATOR);
+            final FloatProp mNavFadeOut = new FloatProp(1f, 0f, clampToDuration(
+                    NAV_FADE_OUT_INTERPOLATOR, 0, ANIMATION_NAV_FADE_OUT_DURATION,
+                    APP_LAUNCH_DURATION));
+            final FloatProp mNavFadeIn = new FloatProp(0f, 1f, clampToDuration(
+                    NAV_FADE_IN_INTERPOLATOR, ANIMATION_DELAY_NAV_FADE_IN,
+                    ANIMATION_NAV_FADE_IN_DURATION, APP_LAUNCH_DURATION));
 
             @Override
             public void onUpdate(float percent, boolean initOnly) {
@@ -1508,11 +1509,10 @@
         float startShadowRadius = areAllTargetsTranslucent(appTargets) ? 0 : mMaxShadowRadius;
         closingAnimator.setDuration(duration);
         closingAnimator.addUpdateListener(new MultiValueUpdateListener() {
-            FloatProp mDy = new FloatProp(0, mClosingWindowTransY, 0, duration, DECELERATE_1_7);
-            FloatProp mScale = new FloatProp(1f, 1f, 0, duration, DECELERATE_1_7);
-            FloatProp mAlpha = new FloatProp(1f, 0f, 25, 125, LINEAR);
-            FloatProp mShadowRadius = new FloatProp(startShadowRadius, 0, 0, duration,
-                    DECELERATE_1_7);
+            FloatProp mDy = new FloatProp(0, mClosingWindowTransY, DECELERATE_1_7);
+            FloatProp mScale = new FloatProp(1f, 1f, DECELERATE_1_7);
+            FloatProp mAlpha = new FloatProp(1f, 0f, clampToDuration(LINEAR, 25, 125, duration));
+            FloatProp mShadowRadius = new FloatProp(startShadowRadius, 0, DECELERATE_1_7);
 
             @Override
             public void onUpdate(float percent, boolean initOnly) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index faa67be..189b687 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -686,15 +686,10 @@
         float toScale = iconSize / mDragIconSize;
         float toAlpha = (target == originalView) ? 1f : 0f;
         MultiValueUpdateListener listener = new MultiValueUpdateListener() {
-            final FloatProp mDx = new FloatProp(fromX, toPosition[0], 0,
-                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.FAST_OUT_SLOW_IN);
-            final FloatProp mDy = new FloatProp(fromY, toPosition[1], 0,
-                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR,
-                    FAST_OUT_SLOW_IN);
-            final FloatProp mScale = new FloatProp(1f, toScale, 0,
-                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, FAST_OUT_SLOW_IN);
-            final FloatProp mAlpha = new FloatProp(1f, toAlpha, 0,
-                    ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.ACCELERATE_2);
+            final FloatProp mDx = new FloatProp(fromX, toPosition[0], FAST_OUT_SLOW_IN);
+            final FloatProp mDy = new FloatProp(fromY, toPosition[1], FAST_OUT_SLOW_IN);
+            final FloatProp mScale = new FloatProp(1f, toScale, FAST_OUT_SLOW_IN);
+            final FloatProp mAlpha = new FloatProp(1f, toAlpha, Interpolators.ACCELERATE_2);
             @Override
             public void onUpdate(float percent, boolean initOnly) {
                 animListener.updateDragShadow(mDx.value, mDy.value, mScale.value, mAlpha.value);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
index 369ff14..6713964 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
@@ -22,6 +22,7 @@
 
 import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
 import static com.android.launcher3.LauncherPrefs.PRIVATE_SPACE_APPS;
+import static com.android.launcher3.config.FeatureFlags.LPNH_EXTRA_TOUCH_WIDTH_DP;
 import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_DELAY;
 import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_END_SCALE_PERCENT;
 import static com.android.launcher3.config.FeatureFlags.LPNH_HAPTIC_HINT_ITERATIONS;
@@ -359,6 +360,10 @@
                     "Slop multiplier (applied to edge slop, "
                             + "which is generally already 50% higher than touch slop)",
                     25, 200, 100, LPNH_SLOP_PERCENTAGE));
+            category.addPreference(createSeekBarPreference(
+                    "Extra width DP (how far outside the sides of the nav bar to trigger)",
+                    // Stashed taskbar is currently 220dp; -86 (x2) would result in 48dp touch area.
+                    -86, 100, 1, LPNH_EXTRA_TOUCH_WIDTH_DP));
             category.addPreference(createSeekBarPreference("LPNH timeout",
                     100, 500, 1, LPNH_TIMEOUT_MS));
         }
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 312cdc9..cc582d1 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.Snackbar;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskThumbnailView;
@@ -210,13 +211,19 @@
             }
         }
 
-        private void enterSplitSelect() {
+        protected void enterSplitSelect() {
             RecentsView overviewPanel = mThumbnailView.getTaskView().getRecentsView();
             // Task has already been dismissed
             if (overviewPanel == null) return;
             overviewPanel.initiateSplitSelect(mThumbnailView.getTaskView());
         }
 
+        protected void saveAppPair() {
+            GroupedTaskView taskView = (GroupedTaskView) mThumbnailView.getTaskView();
+            taskView.getRecentsView().getSplitSelectController().getAppPairsController()
+                    .saveAppPair(taskView);
+        }
+
         /**
          * Called when the overlay is no longer used.
          */
@@ -329,6 +336,10 @@
             public void onSplit() {
                 endLiveTileMode(TaskOverlay.this::enterSplitSelect);
             }
+
+            public void onSaveAppPair() {
+                endLiveTileMode(TaskOverlay.this::saveAppPair);
+            }
         }
     }
 
@@ -342,5 +353,8 @@
 
         /** User wants to start split screen with current app. */
         void onSplit();
+
+        /** User wants to save an app pair with current group of apps. */
+        void onSaveAppPair();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 9c84df8..c1b3a16 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -311,9 +311,21 @@
         @Override
         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
                 TaskIdAttributeContainer taskContainer) {
+            DeviceProfile deviceProfile = activity.getDeviceProfile();
             final TaskView taskView = taskContainer.getTaskView();
+            final RecentsView recentsView = taskView.getRecentsView();
+            boolean isLargeTileFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask();
+            boolean isInExpectedScrollPosition =
+                    recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
+            boolean shouldShowActionsButtonInstead =
+                    isLargeTileFocusedTask && isInExpectedScrollPosition;
 
-            if (!FeatureFlags.enableAppPairs() || !taskView.containsMultipleTasks()) {
+            // No "save app pair" menu item if:
+            // - app pairs feature is not enabled
+            // - the task in question is a single task
+            // - the Overview Actions Button should be visible
+            if (!FeatureFlags.enableAppPairs() || !taskView.containsMultipleTasks()
+                    || shouldShowActionsButtonInstead) {
                 return null;
             }
 
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index e30ea7a..8d4255c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -37,6 +37,7 @@
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
+import static com.android.quickstep.util.AnimUtils.clampToDuration;
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 
 import android.animation.Animator;
@@ -267,10 +268,16 @@
             if (navBarTarget != null) {
                 final Rect cropRect = new Rect();
                 out.addOnFrameListener(new MultiValueUpdateListener() {
-                    FloatProp mNavFadeOut = new FloatProp(1f, 0f, 0,
-                            ANIMATION_NAV_FADE_OUT_DURATION, NAV_FADE_OUT_INTERPOLATOR);
-                    FloatProp mNavFadeIn = new FloatProp(0f, 1f, ANIMATION_DELAY_NAV_FADE_IN,
-                            ANIMATION_NAV_FADE_IN_DURATION, NAV_FADE_IN_INTERPOLATOR);
+                    FloatProp mNavFadeOut = new FloatProp(1f, 0f, clampToDuration(
+                            NAV_FADE_OUT_INTERPOLATOR,
+                            0,
+                            ANIMATION_NAV_FADE_OUT_DURATION,
+                            out.getDuration()));
+                    FloatProp mNavFadeIn = new FloatProp(0f, 1f, clampToDuration(
+                            NAV_FADE_IN_INTERPOLATOR,
+                            ANIMATION_DELAY_NAV_FADE_IN,
+                            ANIMATION_NAV_FADE_IN_DURATION,
+                            out.getDuration()));
 
                     @Override
                     public void onUpdate(float percent, boolean initOnly) {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 41c6f9b..3e731e5 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -117,9 +117,7 @@
 
         setter.setViewBackgroundColor(mActivity.getScrimView(), state.getScrimColor(mActivity),
                 config.getInterpolator(ANIM_SCRIM_FADE, LINEAR));
-
-        RecentsState currentState = mActivity.getStateManager().getState();
-        if (isSplitSelectionState(state) && !isSplitSelectionState(currentState)) {
+        if (isSplitSelectionState(state)) {
             int duration = state.getTransitionDuration(mActivity, true /* isToState */);
             // TODO (b/246851887): Pass in setter as a NO_ANIM PendingAnimation instead
             PendingAnimation pa = new PendingAnimation(duration);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index cf8750f..e4a8619 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -22,9 +22,11 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.DisplayController;
@@ -39,6 +41,8 @@
  */
 public class NavHandleLongPressInputConsumer extends DelegateInputConsumer {
 
+    private static final String TAG = "NavHandleLongPressIC";
+
     private final NavHandleLongPressHandler mNavHandleLongPressHandler;
     private final float mNavHandleWidth;
     private final float mScreenWidth;
@@ -175,6 +179,14 @@
 
     private boolean isInNavBarHorizontalArea(float x) {
         float areaFromMiddle = mNavHandleWidth / 2.0f;
+        if (FeatureFlags.CUSTOM_LPNH_THRESHOLDS.get()) {
+            areaFromMiddle += Utilities.dpToPx(FeatureFlags.LPNH_EXTRA_TOUCH_WIDTH_DP.get());
+        }
+        int minAccessibleSize = Utilities.dpToPx(24);  // Half of 48dp because this is per side.
+        if (areaFromMiddle < minAccessibleSize) {
+            Log.w(TAG, "Custom nav handle region is too small - resetting to 48dp");
+            areaFromMiddle = minAccessibleSize;
+        }
         float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
 
         return distFromMiddle < areaFromMiddle;
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 545a94d..f89888a 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -41,6 +41,7 @@
 import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
@@ -87,7 +88,7 @@
     private static final int FEEDBACK_ANIMATION_MS = 133;
     private static final int RIPPLE_VISIBLE_MS = 300;
     private static final int GESTURE_ANIMATION_DELAY_MS = 1500;
-    private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 2000;
+    private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 3000;
     private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000;
     protected float mExitingAppEndingCornerRadius;
     protected float mExitingAppStartingCornerRadius;
@@ -209,8 +210,12 @@
                                 mFeedbackView.removeCallbacks(mFeedbackViewCallback);
                             }
                             mFeedbackViewCallback = mTutorialFragment::continueTutorial;
-                            mFeedbackView.postDelayed(mFeedbackViewCallback,
-                                    ADVANCE_TUTORIAL_TIMEOUT_MS);
+                            mFeedbackView.postDelayed(
+                                    mFeedbackViewCallback,
+                                    AccessibilityManager.getInstance(mContext)
+                                            .getRecommendedTimeoutMillis(
+                                                    ADVANCE_TUTORIAL_TIMEOUT_MS,
+                                                    AccessibilityManager.FLAG_CONTENT_TEXT));
                         }
                     })
                     .start();
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 1f2a02c..8e3d44f 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -16,10 +16,12 @@
 
 package com.android.quickstep.util;
 
+import static com.android.app.animation.Interpolators.clampToProgress;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.os.Bundle;
 import android.os.IRemoteCallback;
+import android.view.animation.Interpolator;
 
 import com.android.launcher3.util.RunnableList;
 
@@ -67,4 +69,15 @@
             }
         };
     }
+
+    /**
+     * Returns a function that runs the given interpolator such that the entire progress is set
+     * between the given duration. That is, we set the interpolation to 0 until startDelay and reach
+     * 1 by (startDelay + duration).
+     */
+    public static Interpolator clampToDuration(Interpolator interpolator, float startDelay,
+            float duration, float totalDuration) {
+        return clampToProgress(interpolator, startDelay / totalDuration,
+                (startDelay + duration) / totalDuration);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/AppCloseConfig.java b/quickstep/src/com/android/quickstep/util/AppCloseConfig.java
deleted file mode 100644
index bec3379..0000000
--- a/quickstep/src/com/android/quickstep/util/AppCloseConfig.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.FloatRange;
-import android.annotation.IntRange;
-
-/*
- * Adds getter methods to {@link MultiValueUpdateListener} specific to app close animation,
- * so that the entire animation can be defined in one place.
- */
-public abstract class AppCloseConfig extends MultiValueUpdateListener {
-
-    /**
-     * Returns the translation y of the workspace contents.
-     */
-    public abstract float getWorkspaceTransY();
-
-    /*
-     * Returns the scale of the workspace contents.
-     */
-    public abstract float getWorkspaceScale();
-
-    /*
-     * Returns the alpha of the window.
-     */
-    public abstract @FloatRange(from = 0, to = 1) float getWindowAlpha();
-
-    /*
-     * Returns the alpha of the foreground layer of an adaptive icon.
-     */
-    public abstract @IntRange(from = 0, to = 255) int getFgAlpha();
-
-    /*
-     * Returns the corner radius of the window and icon.
-     */
-    public abstract float getCornerRadius();
-
-    /*
-     * Returns the interpolated progress of the animation.
-     */
-    public abstract float getInterpolatedProgress();
-
-}
diff --git a/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
index 1c3c9c2..72fc2a6 100644
--- a/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
+++ b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
@@ -18,6 +18,8 @@
 import android.animation.ValueAnimator;
 import android.view.animation.Interpolator;
 
+import com.android.launcher3.Utilities;
+
 import java.util.ArrayList;
 
 /**
@@ -31,14 +33,11 @@
     @Override
     public final void onAnimationUpdate(ValueAnimator animator) {
         final float percent = animator.getAnimatedFraction();
-        final float currentPlayTime = percent * animator.getDuration();
 
         for (int i = mAllProperties.size() - 1; i >= 0; i--) {
             FloatProp prop = mAllProperties.get(i);
-            float time = Math.max(0, currentPlayTime - prop.mDelay);
-            float newPercent = Math.min(1f, time / prop.mDuration);
-            newPercent = prop.mInterpolator.getInterpolation(newPercent);
-            prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent);
+            float interpolatedPercent = prop.mInterpolator.getInterpolation(percent);
+            prop.value = Utilities.mapRange(interpolatedPercent, prop.mStart, prop.mEnd);
         }
         onUpdate(percent, false /* initOnly */);
     }
@@ -55,17 +54,12 @@
 
         private final float mStart;
         private final float mEnd;
-        private final float mDelay;
-        private final float mDuration;
         private final Interpolator mInterpolator;
 
-        public FloatProp(float start, float end, float delay, float duration, Interpolator i) {
+        public FloatProp(float start, float end, Interpolator i) {
             value = mStart = start;
             mEnd = end;
-            mDelay = delay;
-            mDuration = duration;
             mInterpolator = i;
-
             mAllProperties.add(this);
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index b7b1d8f..8f5c9c1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -70,69 +70,84 @@
 import java.util.function.Supplier
 
 /**
- * Utils class to help run animations for initiating split screen from launcher.
- * Will be expanded with future refactors. Works in conjunction with the state stored in
- * [SplitSelectStateController]
+ * Utils class to help run animations for initiating split screen from launcher. Will be expanded
+ * with future refactors. Works in conjunction with the state stored in [SplitSelectStateController]
  */
 class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) {
     companion object {
         // Break this out into maybe enums? Abstractions into its own classes? Tbd.
         data class SplitAnimInitProps(
-                val originalView: View,
-                val originalBitmap: Bitmap?,
-                val iconDrawable: Drawable,
-                val fadeWithThumbnail: Boolean,
-                val isStagedTask: Boolean,
-                val iconView: View?
+            val originalView: View,
+            val originalBitmap: Bitmap?,
+            val iconDrawable: Drawable,
+            val fadeWithThumbnail: Boolean,
+            val isStagedTask: Boolean,
+            val iconView: View?
         )
     }
 
     /**
-     * Returns different elements to animate for the initial split selection animation
-     * depending on the state of the surface from which the split was initiated
+     * Returns different elements to animate for the initial split selection animation depending on
+     * the state of the surface from which the split was initiated
      */
-    fun getFirstAnimInitViews(taskViewSupplier: Supplier<TaskView>,
-                              splitSelectSourceSupplier: Supplier<SplitSelectSource?>)
-            : SplitAnimInitProps {
+    fun getFirstAnimInitViews(
+        taskViewSupplier: Supplier<TaskView>,
+        splitSelectSourceSupplier: Supplier<SplitSelectSource?>
+    ): SplitAnimInitProps {
         val splitSelectSource = splitSelectSourceSupplier.get()
         if (!splitSelectStateController.isAnimateCurrentTaskDismissal) {
             // Initiating from home
-            return SplitAnimInitProps(splitSelectSource!!.view, originalBitmap = null,
-                    splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true,
-                    iconView = null)
+            return SplitAnimInitProps(
+                splitSelectSource!!.view,
+                originalBitmap = null,
+                splitSelectSource.drawable,
+                fadeWithThumbnail = false,
+                isStagedTask = true,
+                iconView = null
+            )
         } else if (splitSelectStateController.isDismissingFromSplitPair) {
             // Initiating split from overview, but on a split pair
             val taskView = taskViewSupplier.get()
-            for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) {
+            for (container: TaskIdAttributeContainer in taskView.taskIdAttributeContainers) {
                 if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) {
                     val drawable = getDrawable(container.iconView, splitSelectSource)
-                    return SplitAnimInitProps(container.thumbnailView,
-                            container.thumbnailView.thumbnail, drawable!!,
-                            fadeWithThumbnail = true, isStagedTask = true,
-                            iconView = container.iconView.asView()
+                    return SplitAnimInitProps(
+                        container.thumbnailView,
+                        container.thumbnailView.thumbnail,
+                        drawable!!,
+                        fadeWithThumbnail = true,
+                        isStagedTask = true,
+                        iconView = container.iconView.asView()
                     )
                 }
             }
-            throw IllegalStateException("Attempting to init split from existing split pair " +
-                    "without a valid taskIdAttributeContainer")
+            throw IllegalStateException(
+                "Attempting to init split from existing split pair " +
+                    "without a valid taskIdAttributeContainer"
+            )
         } else {
             // Initiating split from overview on fullscreen task TaskView
             val taskView = taskViewSupplier.get()
             val drawable = getDrawable(taskView.iconView, splitSelectSource)
-            return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail,
-                    drawable!!, fadeWithThumbnail = true, isStagedTask = true,
-                    taskView.iconView.asView()
+            return SplitAnimInitProps(
+                taskView.thumbnail,
+                taskView.thumbnail.thumbnail,
+                drawable!!,
+                fadeWithThumbnail = true,
+                isStagedTask = true,
+                taskView.iconView.asView()
             )
         }
     }
 
     /**
-     * Returns the drawable that's provided in iconView, however if that
-     * is null it falls back to the drawable that's in splitSelectSource.
-     * TaskView's icon drawable can be null if the TaskView is scrolled far enough off screen
+     * Returns the drawable that's provided in iconView, however if that is null it falls back to
+     * the drawable that's in splitSelectSource. TaskView's icon drawable can be null if the
+     * TaskView is scrolled far enough off screen
+     *
      * @return [Drawable]
      */
-    fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?) : Drawable? {
+    fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable? {
         if (iconView.drawable == null && splitSelectSource != null) {
             return splitSelectSource.drawable
         }
@@ -140,21 +155,25 @@
     }
 
     /**
-     * When selecting first app from split pair, second app's thumbnail remains. This animates
-     * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying
-     * it with [TaskThumbnailView]'s splashView. Adds animations to the provided builder.
-     * Note: The app that **was not** selected as the first split app should be the container that's
-     * passed through.
+     * When selecting first app from split pair, second app's thumbnail remains. This animates the
+     * second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying it
+     * with [TaskThumbnailView]'s splashView. Adds animations to the provided builder. Note: The app
+     * that **was not** selected as the first split app should be the container that's passed
+     * through.
      *
      * @param builder Adds animation to this
      * @param taskIdAttributeContainer container of the app that **was not** selected
      * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair
-     *                               (opposite of that representing [taskIdAttributeContainer])
+     *   (opposite of that representing [taskIdAttributeContainer])
      */
-    fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer,
-                                builder: PendingAnimation, deviceProfile: DeviceProfile,
-                                taskViewWidth: Int, taskViewHeight: Int,
-                                isPrimaryTaskSplitting: Boolean) {
+    fun addInitialSplitFromPair(
+        taskIdAttributeContainer: TaskIdAttributeContainer,
+        builder: PendingAnimation,
+        deviceProfile: DeviceProfile,
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        isPrimaryTaskSplitting: Boolean
+    ) {
         val thumbnail = taskIdAttributeContainer.thumbnailView
         val iconView: View = taskIdAttributeContainer.iconView.asView()
         builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f))
@@ -170,35 +189,42 @@
                 )
             )
             builder.add(
-                ObjectAnimator.ofFloat(
-                    iconView.splitTranslationY,
-                    MULTI_PROPERTY_VALUE,
-                    0f
-                )
+                ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f)
             )
         }
         if (deviceProfile.isLeftRightSplit) {
             // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
             val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f
             val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width
-            builder.add(ObjectAnimator.ofFloat(thumbnail,
-                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX))
+            builder.add(
+                ObjectAnimator.ofFloat(
+                    thumbnail,
+                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X,
+                    centerThumbnailTranslationX
+                )
+            )
             if (!enableOverviewIconMenu()) {
                 // icons are anchored from Gravity.END, so need to use negative translation
                 val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f
-                builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
-                    -centerIconTranslationX))
+                builder.add(
+                    ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX)
+                )
             }
             builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX))
 
             // Reset other dimensions
             // TODO(b/271468547), can't set Y translate to 0, need to account for top space
             thumbnail.scaleY = 1f
-            val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else
-                deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
-            builder.add(ObjectAnimator.ofFloat(thumbnail,
+            val translateYResetVal: Float =
+                if (!isPrimaryTaskSplitting) 0f
+                else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
+            builder.add(
+                ObjectAnimator.ofFloat(
+                    thumbnail,
                     TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y,
-                    translateYResetVal))
+                    translateYResetVal
+                )
+            )
         } else {
             val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
             // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0
@@ -214,16 +240,21 @@
             //  translations otherwise this asymmetry causes problems..
             if (isPrimaryTaskSplitting) {
                 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
-                centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx
-                        .toFloat()
+                centerThumbnailTranslationY +=
+                    deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
             } else {
                 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
             }
             val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height
-            builder.add(ObjectAnimator.ofFloat(thumbnail,
-                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY))
+            builder.add(
+                ObjectAnimator.ofFloat(
+                    thumbnail,
+                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y,
+                    centerThumbnailTranslationY
+                )
+            )
 
-            if (!enableOverviewIconMenu())  {
+            if (!enableOverviewIconMenu()) {
                 // icons are anchored from Gravity.END, so need to use negative translation
                 builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f))
             }
@@ -231,8 +262,9 @@
 
             // Reset other dimensions
             thumbnail.scaleX = 1f
-            builder.add(ObjectAnimator.ofFloat(thumbnail,
-                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f))
+            builder.add(
+                ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f)
+            )
         }
     }
 
@@ -250,69 +282,94 @@
      * Returns [AnimatorSet] which slides initial split placeholder view offscreen and logs an event
      * for why split is being dismissed
      */
-    fun createPlaceholderDismissAnim(launcher: StatefulActivity<*>,
-                                     splitDismissEvent: EventEnum,
-                                     duration: Long?) : AnimatorSet {
+    fun createPlaceholderDismissAnim(
+        launcher: StatefulActivity<*>,
+        splitDismissEvent: EventEnum,
+        duration: Long?
+    ): AnimatorSet {
         val animatorSet = AnimatorSet()
         duration?.let { animatorSet.duration = it }
-        val recentsView : RecentsView<*, *> = launcher.getOverviewPanel()
-        val floatingTask: FloatingTaskView = splitSelectStateController.firstFloatingTaskView
-                ?: return animatorSet
+        val recentsView: RecentsView<*, *> = launcher.getOverviewPanel()
+        val floatingTask: FloatingTaskView =
+            splitSelectStateController.firstFloatingTaskView ?: return animatorSet
 
         // We are in split selection state currently, transitioning to another state
         val dragLayer: BaseDragLayer<*> = launcher.dragLayer
         val onScreenRectF = RectF()
-        Utilities.getBoundsForViewInDragLayer(dragLayer, floatingTask,
-                Rect(0, 0, floatingTask.width, floatingTask.height),
-                false, null, onScreenRectF)
+        Utilities.getBoundsForViewInDragLayer(
+            dragLayer,
+            floatingTask,
+            Rect(0, 0, floatingTask.width, floatingTask.height),
+            false,
+            null,
+            onScreenRectF
+        )
         // Get the part of the floatingTask that intersects with the DragLayer (i.e. the
         // on-screen portion)
         onScreenRectF.intersect(
-                dragLayer.left.toFloat(),
-                dragLayer.top.toFloat(),
-                dragLayer.right.toFloat(),
-                dragLayer.bottom
-                        .toFloat()
+            dragLayer.left.toFloat(),
+            dragLayer.top.toFloat(),
+            dragLayer.right.toFloat(),
+            dragLayer.bottom.toFloat()
         )
-        animatorSet.play(ObjectAnimator.ofFloat(floatingTask,
+        animatorSet.play(
+            ObjectAnimator.ofFloat(
+                floatingTask,
                 FloatingTaskView.PRIMARY_TRANSLATE_OFFSCREEN,
-                recentsView.pagedOrientationHandler
-                        .getFloatingTaskOffscreenTranslationTarget(
-                                floatingTask,
-                                onScreenRectF,
-                                floatingTask.stagePosition,
-                                launcher.deviceProfile
-                        )))
-        animatorSet.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator) {
-                splitSelectStateController.resetState()
-                safeRemoveViewFromDragLayer(launcher,
-                        splitSelectStateController.splitInstructionsView)
+                recentsView.pagedOrientationHandler.getFloatingTaskOffscreenTranslationTarget(
+                    floatingTask,
+                    onScreenRectF,
+                    floatingTask.stagePosition,
+                    launcher.deviceProfile
+                )
+            )
+        )
+        animatorSet.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    splitSelectStateController.resetState()
+                    safeRemoveViewFromDragLayer(
+                        launcher,
+                        splitSelectStateController.splitInstructionsView
+                    )
+                }
             }
-        })
+        )
         splitSelectStateController.logExitReason(splitDismissEvent)
         return animatorSet
     }
 
     /**
-     * Returns a [PendingAnimation] to animate in the chip to instruct a user to select a second
-     * app for splitscreen
+     * Returns a [PendingAnimation] to animate in the chip to instruct a user to select a second app
+     * for splitscreen
      */
-    fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>) : PendingAnimation {
+    fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>): PendingAnimation {
         safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
         val splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
         splitSelectStateController.splitInstructionsView = splitInstructionsView
         val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
         val anim = PendingAnimation(100 /*duration */)
         splitInstructionsView.alpha = 0f
-        anim.setViewAlpha(splitInstructionsView, 1f,
-                Interpolators.clampToProgress(Interpolators.LINEAR,
-                        timings.instructionsContainerFadeInStartOffset,
-                        timings.instructionsContainerFadeInEndOffset))
-        anim.addFloat(splitInstructionsView, SplitInstructionsView.UNFOLD, 0.1f, 1f,
-                Interpolators.clampToProgress(Interpolators.EMPHASIZED_DECELERATE,
-                        timings.instructionsUnfoldStartOffset,
-                        timings.instructionsUnfoldEndOffset))
+        anim.setViewAlpha(
+            splitInstructionsView,
+            1f,
+            Interpolators.clampToProgress(
+                Interpolators.LINEAR,
+                timings.instructionsContainerFadeInStartOffset,
+                timings.instructionsContainerFadeInEndOffset
+            )
+        )
+        anim.addFloat(
+            splitInstructionsView,
+            SplitInstructionsView.UNFOLD,
+            0.1f,
+            1f,
+            Interpolators.clampToProgress(
+                Interpolators.EMPHASIZED_DECELERATE,
+                timings.instructionsUnfoldStartOffset,
+                timings.instructionsUnfoldEndOffset
+            )
+        )
         return anim
     }
 
@@ -323,15 +380,20 @@
 
     /**
      * Animates the first placeholder view to fullscreen and launches its task.
+     *
      * TODO(b/276361926): Remove the [resetCallback] option once contextual launches
      */
-    fun playAnimPlaceholderToFullscreen(launcher: StatefulActivity<*>, view: View,
-                                        resetCallback: Optional<Runnable>) {
+    fun playAnimPlaceholderToFullscreen(
+        launcher: StatefulActivity<*>,
+        view: View,
+        resetCallback: Optional<Runnable>
+    ) {
         val stagedTaskView = view as FloatingTaskView
 
         val isTablet: Boolean = launcher.deviceProfile.isTablet
-        val duration = if (isTablet) SplitAnimationTimings.TABLET_CONFIRM_DURATION else
-            SplitAnimationTimings.PHONE_CONFIRM_DURATION
+        val duration =
+            if (isTablet) SplitAnimationTimings.TABLET_CONFIRM_DURATION
+            else SplitAnimationTimings.PHONE_CONFIRM_DURATION
         val pendingAnimation = PendingAnimation(duration.toLong())
         val firstTaskStartingBounds = Rect()
         val firstTaskEndingBounds = Rect()
@@ -341,11 +403,12 @@
         splitSelectStateController.setLaunchingFirstAppFullscreen()
 
         stagedTaskView.addConfirmAnimation(
-                pendingAnimation,
-                RectF(firstTaskStartingBounds),
-                firstTaskEndingBounds,
-                false /* fadeWithThumbnail */,
-                true /* isStagedTask */)
+            pendingAnimation,
+            RectF(firstTaskStartingBounds),
+            firstTaskEndingBounds,
+            false /* fadeWithThumbnail */,
+            true /* isStagedTask */
+        )
 
         pendingAnimation.addEndListener {
             splitSelectStateController.launchInitialAppFullscreen {
@@ -490,8 +553,8 @@
      * When the user taps an app pair icon to launch split, this will play the tasks' launch
      * animation from the position of the icon.
      *
-     * To find the root shell leash that we want to fade in, we do the following:
-     * The Changes we receive in transitionInfo are structured like this
+     * To find the root shell leash that we want to fade in, we do the following: The Changes we
+     * receive in transitionInfo are structured like this
      *
      *     Root (grandparent)
      *     |
@@ -503,9 +566,9 @@
      *         |
      *          --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
      *
-     * We want to animate the Root (grandparent) so that it affects both apps and the divider.
-     * To do this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the
-     * left-side ones, for simplicity) and traverse the tree until we find the grandparent.
+     * We want to animate the Root (grandparent) so that it affects both apps and the divider. To do
+     * this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the left-side ones,
+     * for simplicity) and traverse the tree until we find the grandparent.
      *
      * This function is only called when we are animating the app pair in from scratch. It is NOT
      * called when we are animating in from an existing visible TaskView tile or an app that is
@@ -544,8 +607,10 @@
             // TODO (b/316490565): Replace this logic when SplitBounds is available to
             //  startAnimation() and we can know the precise taskIds of launching tasks.
             // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
-            if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
-                (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)) {
+            if (
+                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
+                    (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
+            ) {
                 // Check if it is a left/top app.
                 val isLeftTopApp =
                     (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
@@ -614,8 +679,6 @@
                     FloatProp(
                         floatingView.startingPosition.left,
                         dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
-                        0f /* delay */,
-                        timings.getDuration().toFloat(),
                         Interpolators.clampToProgress(
                             timings.getStagedRectXInterpolator(),
                             timings.stagedRectSlideStartOffset,
@@ -626,8 +689,6 @@
                     FloatProp(
                         floatingView.startingPosition.top,
                         dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
-                        0f /* delay */,
-                        timings.getDuration().toFloat(),
                         Interpolators.clampToProgress(
                             Interpolators.EMPHASIZED,
                             timings.stagedRectSlideStartOffset,
@@ -638,8 +699,6 @@
                     FloatProp(
                         1f /* start */,
                         dp.widthPx / floatingView.startingPosition.width(),
-                        0f /* delay */,
-                        timings.getDuration().toFloat(),
                         Interpolators.clampToProgress(
                             Interpolators.EMPHASIZED,
                             timings.stagedRectSlideStartOffset,
@@ -650,8 +709,6 @@
                     FloatProp(
                         1f /* start */,
                         dp.heightPx / floatingView.startingPosition.height(),
-                        0f /* delay */,
-                        timings.getDuration().toFloat(),
                         Interpolators.clampToProgress(
                             Interpolators.EMPHASIZED,
                             timings.stagedRectSlideStartOffset,
diff --git a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
index fdc8f1f..121d8ed 100644
--- a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
+++ b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_EDU_SHOWN;
+import static com.android.quickstep.util.AnimUtils.clampToDuration;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -187,10 +188,14 @@
         intro.setInterpolator(LINEAR);
         intro.setDuration(introDuration);
         intro.addUpdateListener((new MultiValueUpdateListener() {
-            FloatProp mCircleAlpha = new FloatProp(0, 255, 0, firstPart, LINEAR);
-            FloatProp mCircleScale = new FloatProp(2f, 1f, 0, firstPart, OVERSHOOT_1_7);
-            FloatProp mDeltaY = new FloatProp(0, transY, firstPart, secondPart, FAST_OUT_SLOW_IN);
-            FloatProp mGradientAlpha = new FloatProp(0, 255, firstPart, secondPart * 0.3f, LINEAR);
+            FloatProp mCircleAlpha = new FloatProp(0, 255,
+                    clampToDuration(LINEAR, 0, firstPart, introDuration));
+            FloatProp mCircleScale = new FloatProp(2f, 1f,
+                    clampToDuration(OVERSHOOT_1_7, 0, firstPart, introDuration));
+            FloatProp mDeltaY = new FloatProp(0, transY,
+                    clampToDuration(FAST_OUT_SLOW_IN, firstPart, secondPart, introDuration));
+            FloatProp mGradientAlpha = new FloatProp(0, 255,
+                    clampToDuration(LINEAR, firstPart, secondPart * 0.3f, introDuration));
 
             @Override
             public void onUpdate(float progress, boolean initOnly) {
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index 12a073f..18922a6 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -328,20 +328,20 @@
 
         MultiValueUpdateListener listener = new MultiValueUpdateListener() {
             // SplitPlaceholderView: rectangle translates and stretches to new position
-            final FloatProp mDx = new FloatProp(0, prop.dX, 0, animDuration,
+            final FloatProp mDx = new FloatProp(0, prop.dX,
                     clampToProgress(timings.getStagedRectXInterpolator(),
                             timings.getStagedRectSlideStartOffset(),
                             timings.getStagedRectSlideEndOffset()));
-            final FloatProp mDy = new FloatProp(0, prop.dY, 0, animDuration,
+            final FloatProp mDy = new FloatProp(0, prop.dY,
                     clampToProgress(timings.getStagedRectYInterpolator(),
                             timings.getStagedRectSlideStartOffset(),
                             timings.getStagedRectSlideEndOffset()));
-            final FloatProp mTaskViewScaleX = new FloatProp(1f, prop.finalTaskViewScaleX, 0,
-                    animDuration, clampToProgress(timings.getStagedRectScaleXInterpolator(),
+            final FloatProp mTaskViewScaleX = new FloatProp(1f, prop.finalTaskViewScaleX,
+                    clampToProgress(timings.getStagedRectScaleXInterpolator(),
                     timings.getStagedRectSlideStartOffset(),
                     timings.getStagedRectSlideEndOffset()));
-            final FloatProp mTaskViewScaleY = new FloatProp(1f, prop.finalTaskViewScaleY, 0,
-                    animDuration, clampToProgress(timings.getStagedRectScaleYInterpolator(),
+            final FloatProp mTaskViewScaleY = new FloatProp(1f, prop.finalTaskViewScaleY,
+                    clampToProgress(timings.getStagedRectScaleYInterpolator(),
                     timings.getStagedRectSlideStartOffset(),
                     timings.getStagedRectSlideEndOffset()));
             @Override
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 10ef47c..259927d 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -24,6 +24,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -40,11 +41,11 @@
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
 
-import kotlin.Unit;
-
 import java.util.HashMap;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
  *
@@ -382,7 +383,11 @@
 
     @Override
     public void setOverlayEnabled(boolean overlayEnabled) {
-        // Intentional no-op to prevent setting smart actions overlay on thumbnails
+        if (FeatureFlags.enableAppPairs()) {
+            super.setOverlayEnabled(overlayEnabled);
+        } else {
+            // Intentional no-op to prevent setting smart actions overlay on thumbnails
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 8281ad7..7a1c49a 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -92,14 +93,28 @@
     private static final int INDEX_SCROLL_ALPHA = 5;
     private static final int NUM_ALPHAS = 6;
 
+    public @interface ScreenshotButtonHiddenFlags { }
+    public static final int FLAG_MULTIPLE_TASKS_HIDE_SCREENSHOT = 1 << 0;
+
     public @interface SplitButtonHiddenFlags { }
-    public static final int FLAG_IS_NOT_TABLET = 1 << 0;
+    public static final int FLAG_SMALL_SCREEN_HIDE_SPLIT = 1 << 0;
+    public static final int FLAG_MULTIPLE_TASKS_HIDE_SPLIT = 1 << 1;
 
     public @interface SplitButtonDisabledFlags { }
-    public static final int FLAG_SINGLE_TASK = 1 << 0;
+    public static final int FLAG_SINGLE_TASK_DISABLE_SPLIT = 1 << 0;
+
+    public @interface AppPairButtonHiddenFlags { }
+    public static final int FLAG_SINGLE_TASK_HIDE_APP_PAIR = 1 << 0;
+    public static final int FLAG_SMALL_SCREEN_HIDE_APP_PAIR = 1 << 1;
 
     private MultiValueAlpha mMultiValueAlpha;
+
+    // The screenshot button is implemented as a Button in launcher3 and NexusLauncher, but is an
+    // ImageButton in go launcher (does not share a common class with Button). Take care when
+    // casting this.
+    private View mScreenshotButton;
     private Button mSplitButton;
+    private Button mSaveAppPairButton;
 
     @ActionsHiddenFlags
     private int mHiddenFlags;
@@ -107,11 +122,14 @@
     @ActionsDisabledFlags
     protected int mDisabledFlags;
 
+    @ScreenshotButtonHiddenFlags
+    private int mScreenshotButtonHiddenFlags;
+
     @SplitButtonHiddenFlags
     private int mSplitButtonHiddenFlags;
 
-    @SplitButtonDisabledFlags
-    private int mSplitButtonDisabledFlags;
+    @AppPairButtonHiddenFlags
+    private int mAppPairButtonHiddenFlags;
 
     @Nullable
     protected T mCallbacks;
@@ -138,9 +156,12 @@
         mMultiValueAlpha = new MultiValueAlpha(findViewById(R.id.action_buttons), NUM_ALPHAS);
         mMultiValueAlpha.setUpdateVisibility(true);
 
-        findViewById(R.id.action_screenshot).setOnClickListener(this);
+        mScreenshotButton = findViewById(R.id.action_screenshot);
+        mScreenshotButton.setOnClickListener(this);
         mSplitButton = findViewById(R.id.action_split);
         mSplitButton.setOnClickListener(this);
+        mSaveAppPairButton = findViewById(R.id.action_save_app_pair);
+        mSaveAppPairButton.setOnClickListener(this);
     }
 
     /**
@@ -162,6 +183,8 @@
             mCallbacks.onScreenshot();
         } else if (id == R.id.action_split) {
             mCallbacks.onSplit();
+        } else if (id == R.id.action_save_app_pair) {
+            mCallbacks.onSaveAppPair();
         }
     }
 
@@ -204,7 +227,49 @@
         }
         boolean isEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
         LayoutUtils.setViewEnabled(this, isEnabled);
-        updateSplitButtonEnabledState();
+    }
+
+    /**
+     * Updates a batch of flags to hide and show actions buttons when a grouped task (split screen)
+     * is focused.
+     * @param isGroupedTask True if the focused task is a grouped task.
+     */
+    public void updateForGroupedTask(boolean isGroupedTask) {
+        // Update flags to see if split button should be hidden.
+        updateSplitButtonHiddenFlags(FLAG_MULTIPLE_TASKS_HIDE_SPLIT, isGroupedTask);
+        // Update flags to see if screenshot button should be hidden.
+        updateScreenshotButtonHiddenFlags(FLAG_MULTIPLE_TASKS_HIDE_SCREENSHOT, isGroupedTask);
+        // Update flags to see if save app pair button should be hidden.
+        updateAppPairButtonHiddenFlags(FLAG_SINGLE_TASK_HIDE_APP_PAIR, !isGroupedTask);
+    }
+
+    /**
+     * Updates a batch of flags to hide and show actions buttons for tablet/non tablet case.
+     * @param isSmallScreen True if the current display is a small screen.
+     */
+    public void updateForSmallScreen(boolean isSmallScreen) {
+        // Update flags to see if split button should be hidden.
+        updateSplitButtonHiddenFlags(FLAG_SMALL_SCREEN_HIDE_SPLIT, isSmallScreen);
+        // Update flags to see if save app pair button should be hidden.
+        updateAppPairButtonHiddenFlags(FLAG_SMALL_SCREEN_HIDE_APP_PAIR, isSmallScreen);
+    }
+
+    /**
+     * Updates the proper flags to indicate whether the "Screenshot" button should be hidden.
+     *
+     * @param flag   The flag to update.
+     * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
+     */
+    private void updateScreenshotButtonHiddenFlags(@ScreenshotButtonHiddenFlags int flag,
+            boolean enable) {
+        if (mScreenshotButton == null) return;
+        if (enable) {
+            mScreenshotButtonHiddenFlags |= flag;
+        } else {
+            mScreenshotButtonHiddenFlags &= ~flag;
+        }
+        int desiredVisibility = mScreenshotButtonHiddenFlags == 0 ? VISIBLE : GONE;
+        mScreenshotButton.setVisibility(desiredVisibility);
     }
 
     /**
@@ -213,16 +278,17 @@
      * @param flag   The flag to update.
      * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
      */
-    public void updateSplitButtonHiddenFlags(@SplitButtonHiddenFlags int flag, boolean enable) {
+    void updateSplitButtonHiddenFlags(@SplitButtonHiddenFlags int flag,
+            boolean enable) {
+        if (mSplitButton == null) return;
         if (enable) {
             mSplitButtonHiddenFlags |= flag;
         } else {
             mSplitButtonHiddenFlags &= ~flag;
         }
-        if (mSplitButton == null) return;
-        boolean shouldBeVisible = mSplitButtonHiddenFlags == 0;
-        mSplitButton.setVisibility(shouldBeVisible ? VISIBLE : GONE);
-        findViewById(R.id.action_split_space).setVisibility(shouldBeVisible ? VISIBLE : GONE);
+        int desiredVisibility = mSplitButtonHiddenFlags == 0 ? VISIBLE : GONE;
+        mSplitButton.setVisibility(desiredVisibility);
+        findViewById(R.id.action_split_space).setVisibility(desiredVisibility);
 
         String callStack = Arrays.stream(
                         Log.getStackTraceString(new Exception("thread stacktrace"))
@@ -232,23 +298,30 @@
                 .collect(Collectors.joining("\n"));
         Log.d("b/321291049", "updateSplitButtonHiddenFlags called with flag: " + flag
                 + " enabled: " + enable
-                + " shouldBeVisible: " + shouldBeVisible
+                + " visibility: " + desiredVisibility
                 + " partial trace: \n" + callStack);
     }
 
     /**
-     * Updates the proper flags to indicate whether the "Split screen" button should be disabled.
+     * Updates the proper flags to indicate whether the "Save app pair" button should be disabled.
      *
      * @param flag   The flag to update.
-     * @param enable Whether to enable the disable flag: True will cause view to be disabled.
+     * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
      */
-    public void updateSplitButtonDisabledFlags(@SplitButtonDisabledFlags int flag, boolean enable) {
-        if (enable) {
-            mSplitButtonDisabledFlags |= flag;
-        } else {
-            mSplitButtonDisabledFlags &= ~flag;
+    private void updateAppPairButtonHiddenFlags(
+            @AppPairButtonHiddenFlags int flag, boolean enable) {
+        if (!FeatureFlags.enableAppPairs()) {
+            return;
         }
-        updateSplitButtonEnabledState();
+
+        if (mSaveAppPairButton == null) return;
+        if (enable) {
+            mAppPairButtonHiddenFlags |= flag;
+        } else {
+            mAppPairButtonHiddenFlags &= ~flag;
+        }
+        int desiredVisibility = mAppPairButtonHiddenFlags == 0 ? VISIBLE : GONE;
+        mSaveAppPairButton.setVisibility(desiredVisibility);
     }
 
     public MultiProperty getContentAlpha() {
@@ -326,19 +399,7 @@
                 ? R.drawable.ic_split_horizontal
                 : R.drawable.ic_split_vertical;
         mSplitButton.setCompoundDrawablesRelativeWithIntrinsicBounds(splitIconRes, 0, 0, 0);
+        mSaveAppPairButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                R.drawable.ic_save_app_pair, 0, 0, 0);
     }
-
-    /**
-     * Enables/disables the "Split" button based on the status of mSplitButtonDisabledFlags and
-     * mDisabledFlags.
-     */
-    private void updateSplitButtonEnabledState() {
-        if (mSplitButton == null) {
-            return;
-        }
-        boolean isParentEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
-        boolean shouldBeEnabled = mSplitButtonDisabledFlags == 0 && isParentEnabled;
-        mSplitButton.setEnabled(shouldBeEnabled);
-    }
-
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ea33b4d..6699147 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -62,8 +62,6 @@
 import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP;
 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
-import static com.android.quickstep.views.OverviewActionsView.FLAG_IS_NOT_TABLET;
-import static com.android.quickstep.views.OverviewActionsView.FLAG_SINGLE_TASK;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
@@ -4007,18 +4005,24 @@
     }
 
     /**
-     * Hides all overview actions if current page is for split apps, shows otherwise
-     * If actions are showing, we only show split option if
+     * Hides all overview actions if user is halfway through split selection, shows otherwise.
+     * We only show split option if:
+     * * Focused view is a single app
      * * Device is large screen
-     * * There are at least 2 tasks to invoke split
      */
     private void updateCurrentTaskActionsVisibility() {
         boolean isCurrentSplit = getCurrentPageTaskView() instanceof GroupedTaskView;
-        mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit);
+        // Update flags to see if entire actions bar should be hidden.
+        if (!FeatureFlags.enableAppPairs()) {
+            mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit);
+        }
         mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SELECT_ACTIVE, isSplitSelectionActive());
-        mActionsView.updateSplitButtonHiddenFlags(FLAG_IS_NOT_TABLET,
-                !mActivity.getDeviceProfile().isTablet);
-        mActionsView.updateSplitButtonDisabledFlags(FLAG_SINGLE_TASK, /*enable=*/ false);
+        // Update flags to see if actions bar should show buttons for a single task or a pair of
+        // tasks.
+        mActionsView.updateForGroupedTask(isCurrentSplit);
+        // Update flags to see if actions bar should show buttons for tablets or phones.
+        mActionsView.updateForSmallScreen(!mActivity.getDeviceProfile().isTablet);
+
         if (isDesktopModeSupported()) {
             boolean isCurrentDesktop = getCurrentPageTaskView() instanceof DesktopTaskView;
             mActionsView.updateHiddenFlags(HIDDEN_DESKTOP, isCurrentDesktop);
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index b0a644b..27e084c 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -309,6 +309,13 @@
         val LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE =
             nonRestorableItem("LPNH_SLOP_PERCENTAGE", 100, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
         @JvmField
+        val LONG_PRESS_NAV_HANDLE_EXTRA_TOUCH_WIDTH_DP =
+            nonRestorableItem(
+                "LPNH_EXTRA_TOUCH_WIDTH_DP",
+                0,
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
+        @JvmField
         val LONG_PRESS_NAV_HANDLE_TIMEOUT_MS =
             nonRestorableItem(
                 "LPNH_TIMEOUT_MS",
@@ -349,8 +356,8 @@
         @JvmField
         val PRIVATE_SPACE_APPS =
             nonRestorableItem("pref_private_space_apps", 0, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
-        @JvmField val ENABLE_TWOLINE_ALLAPPS_TOGGLE =
-                backedUpItem("pref_enable_two_line_toggle", false)
+        @JvmField
+        val ENABLE_TWOLINE_ALLAPPS_TOGGLE = backedUpItem("pref_enable_two_line_toggle", false)
         @JvmField
         val THEMED_ICONS =
             backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 072a96c..e25e033 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.config;
 
 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
+import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_EXTRA_TOUCH_WIDTH_DP;
 import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_DELAY;
 import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT;
 import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS;
@@ -147,6 +148,12 @@
                     "Controls touch slop percentage for lpnh",
                     LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE);
 
+    public static final IntFlag LPNH_EXTRA_TOUCH_WIDTH_DP =
+            FlagsFactory.getIntFlag(301680992, "LPNH_EXTRA_TOUCH_WIDTH_DP", 0,
+                    "Controls extra dp on the nav bar sides to trigger LPNH."
+                            + " Can be negative for a smaller touch region.",
+                    LONG_PRESS_NAV_HANDLE_EXTRA_TOUCH_WIDTH_DP);
+
     public static final IntFlag LPNH_TIMEOUT_MS =
             FlagsFactory.getIntFlag(301680992, "LPNH_TIMEOUT_MS",
                     ViewConfiguration.getLongPressTimeout(),
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index af66431..1d44f20 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -105,12 +105,10 @@
      */
     public static boolean migrateGridIfNeeded(
             @NonNull Context context,
-            @NonNull InvariantDeviceProfile idp,
+            @NonNull DeviceGridState srcDeviceState,
+            @NonNull DeviceGridState destDeviceState,
             @NonNull DatabaseHelper target,
             @NonNull SQLiteDatabase source) {
-
-        DeviceGridState srcDeviceState = new DeviceGridState(context);
-        DeviceGridState destDeviceState = new DeviceGridState(idp);
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
             return true;
         }
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index ba2b64d..8ed554a 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -308,8 +308,12 @@
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
                 : createDatabaseHelper(true /* forMigration */);
         try {
-            return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, idp, mOpenHelper,
-                   oldHelper.getWritableDatabase());
+            // This is the current grid we have, given by the mContext
+            DeviceGridState srcDeviceState = new DeviceGridState(mContext);
+            // This is the state we want to migrate to that is given by the idp
+            DeviceGridState destDeviceState = new DeviceGridState(idp);
+            return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, srcDeviceState,
+                    destDeviceState, mOpenHelper, oldHelper.getWritableDatabase());
         } catch (Exception e) {
             FileLog.e(TAG, "Failed to migrate grid", e);
             return false;
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index a6a04a7..59dd1b1 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -78,10 +78,12 @@
 
         if (!matchingWorkspaceItems.isEmpty()) {
             if (mShortcuts.isEmpty()) {
+                PackageManagerHelper packageManagerHelper = new PackageManagerHelper(
+                        app.getContext());
                 // Verify that the app is indeed installed.
-                if (!new PackageManagerHelper(app.getContext())
-                        .isAppInstalled(mPackageName, mUser)) {
-                    // App is not installed, ignoring package events
+                if (!packageManagerHelper.isAppInstalled(mPackageName, mUser)
+                        && !packageManagerHelper.isAppArchivedForUser(mPackageName, mUser)) {
+                    // App is not installed or archived, ignoring package events
                     return;
                 }
             }
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 1231cd7..07df7af 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.testing.shared.TestProtocol.TEST_INFO_RESPONSE_FIELD;
+import static com.android.launcher3.config.FeatureFlags.enableAppPairs;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.app.Activity;
@@ -300,6 +301,11 @@
                 return response;
             }
 
+            case TestProtocol.REQUEST_FLAG_ENABLE_APP_PAIRS: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, enableAppPairs());
+                return response;
+            }
+
             default:
                 return null;
         }
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 11d8e97..606918e 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -105,6 +105,21 @@
     }
 
     /**
+     * Returns whether the target app is archived for a given user
+     */
+    public boolean isAppArchivedForUser(@NonNull final String packageName,
+            @NonNull final UserHandle user) {
+        if (!Utilities.enableSupportForArchiving()) {
+            return false;
+        }
+        final ApplicationInfo info = getApplicationInfo(
+                // LauncherApps does not support long flags currently. Since archived apps are
+                // subset of uninstalled apps, this filter also includes archived apps.
+                packageName, user, PackageManager.MATCH_UNINSTALLED_PACKAGES);
+        return info != null && info.isArchived;
+    }
+
+    /**
      * Returns whether the target app is in archived state
      */
     @SuppressWarnings("NewApi")
@@ -172,7 +187,7 @@
     public void startDetailsActivityForInfo(ItemInfo info, Rect sourceBounds, Bundle opts) {
         if (info instanceof ItemInfoWithIcon
                 && (((ItemInfoWithIcon) info).runtimeStatusFlags
-                    & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+                & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
             ItemInfoWithIcon appInfo = (ItemInfoWithIcon) info;
             mContext.startActivity(ApiWrapper.getAppMarketActivityIntent(mContext,
                     appInfo.getTargetComponent().getPackageName(), Process.myUserHandle()));
@@ -251,6 +266,7 @@
 
     /**
      * Returns true if Launcher has the permission to access shortcuts.
+     *
      * @see LauncherApps#hasShortcutHostPermission()
      */
     public static boolean hasShortcutsPermission(Context context) {
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index bc3a136..7d195fd 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -183,6 +183,7 @@
     public static final String REQUEST_EMULATE_PRINT_DEVICE = "emulate-print-device";
 
     public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
+    public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
 
     public static final String REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED =
             "unstash-bubble-bar-if-stashed";
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
index 1cd8c88..4acdddc 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.ui.widget;
 
+import static com.android.launcher3.util.rule.ShellCommandRule.createEnableInputTransportPublisherRule;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -30,7 +32,9 @@
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
 /**
@@ -40,6 +44,9 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class TaplWidgetPickerTest extends AbstractLauncherUiTest {
+    // b/325377690 : To get the log printed where DOWN key event is getting lost from TAPL.
+    @Rule public final TestRule mEnableInputTransportPublisherRule =
+            createEnableInputTransportPublisherRule();
 
     private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
         return WidgetsFullSheet.getWidgetsView(launcher);
diff --git a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
index 08953fc..977995e 100644
--- a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
+++ b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java
@@ -21,6 +21,7 @@
 
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
+import android.text.TextUtils;
 
 import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
@@ -40,6 +41,9 @@
  */
 public class ShellCommandRule implements TestRule {
 
+    private static final String SETPROP_PREFIX = "setprop";
+    private static final String GETPROP_PREFIX = "getprop";
+    private static final String UNKNOWN = "UNKNOWN";
     private final String mCmd;
     private final String mRevertCommand;
     private final boolean mCheckSuccess;
@@ -62,6 +66,19 @@
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
+                String revertSetPropCmd = null;
+                if (mCmd.startsWith(SETPROP_PREFIX) && mRevertCommand == null) {
+                    // setprop command always follows format : setprop <TAG> <value>
+                    // We are stripping out only the TAG here
+                    String tag = mCmd.split("\\s")[1];
+                    String getpropCmd = GETPROP_PREFIX + " " + tag;
+                    String initialValue = UiDevice.getInstance(
+                            getInstrumentation()).executeShellCommand(getpropCmd);
+                    if (TextUtils.isEmpty(initialValue.trim())) {
+                        initialValue = UNKNOWN;
+                    }
+                    revertSetPropCmd = SETPROP_PREFIX + " " + tag + " " + initialValue;
+                }
                 final String result =
                         UiDevice.getInstance(getInstrumentation()).executeShellCommand(mCmd);
                 if (mCheckSuccess) {
@@ -73,13 +90,15 @@
                 try {
                     base.evaluate();
                 } finally {
-                    if (mRevertCommand != null) {
+                    if (mRevertCommand != null || revertSetPropCmd != null) {
+                        String revertCmd =
+                                mRevertCommand != null ? mRevertCommand : revertSetPropCmd;
                         final String revertResult = UiDevice.getInstance(
                                 getInstrumentation()).executeShellCommand(
-                                mRevertCommand);
+                                revertCmd);
                         if (mCheckSuccess) {
                             Assert.assertTrue(
-                                    "Failed command: " + mRevertCommand
+                                    "Failed command: " + revertCmd
                                             + ", result: " + revertResult,
                                     "Success".equals(result.replaceAll("\\s", "")));
                         }
@@ -122,4 +141,14 @@
         return new ShellCommandRule("settings put global heads_up_notifications_enabled 0",
                 "settings put global heads_up_notifications_enabled 1");
     }
+
+    /**
+     * Enables "InputTransportPublisher" debug flag. This prints the key input events dispatched by
+     * the system server.
+     * adb shell setprop log.tag.InputTransportPublisher DEBUG
+     * See {@link com.android.cts.input.DebugInputRule} for more details.
+     */
+    public static ShellCommandRule createEnableInputTransportPublisherRule() {
+        return new ShellCommandRule("setprop log.tag.InputTransportPublisher DEBUG", null);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index ad95ecf..4f20c57 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -401,8 +401,11 @@
         if (isTablet && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) {
             return false;
         }
-        // Overview actions aren't visible for split screen tasks.
-        return !task.isTaskSplit();
+        if (!mLauncher.isAppPairsEnabled() && task.isTaskSplit()) {
+            // Overview actions aren't visible for split screen tasks.
+            return false;
+        }
+        return true;
     }
 
     /**
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 053b360..70a5336 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1952,6 +1952,11 @@
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    boolean isAppPairsEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_FLAG_ENABLE_APP_PAIRS).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope) {
         sendPointer(downTime, currentTime, action, point, gestureScope,