Merge "Using default timeout for 3-button QS"
diff --git a/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml b/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml
new file mode 100644
index 0000000..9c95497
--- /dev/null
+++ b/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+          http://www.apache.org/licenses/LICENSE-2.0
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+  <solid android:color="@color/gesture_tutorial_fake_previous_task_view_color" />
+</shape>
diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
index 2ff3a5e..9d06dfb 100644
--- a/quickstep/res/layout/gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -31,6 +31,14 @@
         android:visibility="invisible" />
 
     <View
+        android:id="@+id/gesture_tutorial_fake_previous_task_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleX="0.98"
+        android:scaleY="0.98"
+        android:visibility="invisible" />
+
+    <View
         android:id="@+id/gesture_tutorial_fake_task_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index c724318..f02acab 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -167,9 +167,14 @@
     }
 
     @Override
-    protected void handlePendingActivityRequest() {
-        super.handlePendingActivityRequest();
-        if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
+    public void onStateSetEnd(LauncherState state) {
+        super.onStateSetEnd(state);
+        handlePendingActivityRequest();
+    }
+
+    private void handlePendingActivityRequest() {
+        if (mPendingActivityRequestCode != -1 && isInState(NORMAL)
+                && ((getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
             // Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher.
             onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null);
             // ProxyActivityStarter is started with clear task to reset the task after which it
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
index 8f31c22..080633a 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
@@ -52,8 +52,7 @@
     public static Bundle convertDataModelToAppTargetBundle(Context context, BgDataModel dataModel) {
         Bundle bundle = new Bundle();
         ArrayList<AppTargetEvent> events = new ArrayList<>();
-        ArrayList<ItemInfo> workspaceItems = new ArrayList<>(dataModel.workspaceItems);
-        workspaceItems.addAll(dataModel.appWidgets);
+        ArrayList<ItemInfo> workspaceItems = dataModel.getAllWorkspaceItems();
         for (ItemInfo item : workspaceItems) {
             AppTarget target = getAppTargetFromInfo(context, item);
             if (target != null && !isTrackedForPrediction(item)) continue;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 950598c..2d704f8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -35,7 +35,6 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
-import android.util.Log;
 import android.view.View;
 
 import com.android.launcher3.BaseQuickstepLauncher;
@@ -55,7 +54,6 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
 import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
@@ -273,9 +271,6 @@
 
     @Override
     public TouchController[] createTouchControllers() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.1");
-        }
         Mode mode = SysUINavigationMode.getMode(this);
 
         ArrayList<TouchController> list = new ArrayList<>();
@@ -283,9 +278,6 @@
         if (mode == NO_BUTTON) {
             list.add(new NoButtonQuickSwitchTouchController(this));
             list.add(new NavBarToHomeTouchController(this));
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "createTouchControllers.2");
-            }
             list.add(new NoButtonNavbarToOverviewTouchController(this));
         } else {
             if (getDeviceProfile().isVerticalBarLayout()) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index f73e2f2..6b9c340 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -29,7 +29,6 @@
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.ValueAnimator;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
@@ -47,7 +46,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.TouchController;
 import com.android.quickstep.TaskUtils;
@@ -103,37 +101,19 @@
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NavBarToHomeTouchController.canInterceptTouch "
-                    + ev);
-        }
         boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
         if (!cameFromNavBar) {
             return false;
         }
         if (mStartState.overviewUi || mStartState == ALL_APPS) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.PAUSE_NOT_DETECTED,
-                        "NavBarToHomeTouchController.canInterceptTouch true 1 "
-                                + mStartState.overviewUi + " " + (mStartState == ALL_APPS));
-            }
             return true;
         }
         int typeToClose = ENABLE_ALL_APPS_EDU.get() ? TYPE_ALL & ~TYPE_ALL_APPS_EDU : TYPE_ALL;
         if (AbstractFloatingView.getTopOpenViewWithType(mLauncher, typeToClose) != null) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.PAUSE_NOT_DETECTED,
-                        "NavBarToHomeTouchController.canInterceptTouch true 2 "
-                                + AbstractFloatingView.getTopOpenView(mLauncher), new Exception());
-            }
             return true;
         }
         if (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
                 && AssistantUtilities.isExcludedAssistantRunning()) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.PAUSE_NOT_DETECTED,
-                        "NavBarToHomeTouchController.canInterceptTouch true 3");
-            }
             return true;
         }
         return false;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index 702c519..addfe92 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -37,7 +37,6 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -49,7 +48,6 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
@@ -90,9 +88,6 @@
         mRecentsView = l.getOverviewPanel();
         mMotionPauseDetector = new MotionPauseDetector(l);
         mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NoButtonNavbarToOverviewTouchController.ctor");
-        }
     }
 
     @Override
@@ -221,9 +216,6 @@
 
     @Override
     public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "NoButtonNavbarToOverviewTouchController");
-        }
         if (mStartedOverview) {
             if (!mReachedOverview) {
                 mStartDisplacement.set(xDisplacement, yDisplacement);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 9729695..7675a79 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -18,8 +18,6 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -59,6 +57,8 @@
 
     private AnimatorPlaybackController mCurrentAnimation;
     private boolean mCurrentAnimationIsGoingUp;
+    private boolean mAllowGoingUp;
+    private boolean mAllowGoingDown;
 
     private boolean mNoIntercept;
 
@@ -74,7 +74,7 @@
         mRecentsView = activity.getOverviewPanel();
         mIsRtl = Utilities.isRtl(activity.getResources());
         SingleAxisSwipeDetector.Direction dir =
-            mRecentsView.getPagedOrientationHandler().getOppositeSwipeDirection();
+                mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection();
         mDetector = new SingleAxisSwipeDetector(activity, this, dir);
     }
 
@@ -148,15 +148,24 @@
                             break;
                         }
                         mTaskBeingDragged = view;
+                        int upDirection = mRecentsView.getPagedOrientationHandler()
+                                .getUpDirection(mIsRtl);
                         if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
                             // Don't allow swipe down to open if we don't support swipe up
                             // to enter overview.
-                            directionsToDetectScroll = DIRECTION_POSITIVE;
+                            directionsToDetectScroll = upDirection;
+                            mAllowGoingUp = true;
+                            mAllowGoingDown = false;
                         } else {
                             // The task can be dragged up to dismiss it,
                             // and down to open if it's the current page.
-                            directionsToDetectScroll = i == mRecentsView.getCurrentPage()
-                                    ? DIRECTION_BOTH : DIRECTION_POSITIVE;
+                            mAllowGoingUp = true;
+                            if (i == mRecentsView.getCurrentPage()) {
+                                mAllowGoingDown = true;
+                                directionsToDetectScroll = DIRECTION_BOTH;
+                            } else {
+                                directionsToDetectScroll = upDirection;
+                            }
                         }
                         break;
                     }
@@ -189,9 +198,7 @@
             // No need to init
             return;
         }
-        int scrollDirections = mDetector.getScrollDirections();
-        if (goingUp && ((scrollDirections & DIRECTION_POSITIVE) == 0)
-                || !goingUp && ((scrollDirections & DIRECTION_NEGATIVE) == 0)) {
+        if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) {
             // Trying to re-init in an unsupported direction.
             return;
         }
@@ -297,7 +304,8 @@
 
         mCurrentAnimation.setEndAction(this::clearState);
         mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
-                velocity, mEndDisplacement, animationDuration);
+                velocity * orientationHandler.getSecondaryTranslationDirectionFactor(),
+                mEndDisplacement, animationDuration);
     }
 
     private void clearState() {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 4ed2425..f82bc2d 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -15,6 +15,9 @@
  */
 package com.android.quickstep;
 
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
@@ -59,7 +62,6 @@
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.SystemClock;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
@@ -80,7 +82,6 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.tracing.InputConsumerProto;
 import com.android.launcher3.tracing.SwipeHandlerProto;
 import com.android.launcher3.util.TraceHelper;
@@ -324,13 +325,7 @@
 
     protected boolean onActivityInit(Boolean alreadyOnHome) {
         T createdActivity = mActivityInterface.getCreatedActivity();
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
-        }
         if (createdActivity != null) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
-            }
             initTransitionEndpoints(createdActivity.getDeviceProfile());
         }
         final T activity = mActivityInterface.getCreatedActivity();
@@ -732,6 +727,7 @@
         setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
         mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
         mGestureStarted = true;
+        mTaskViewSimulator.setDrawsBelowRecents(true);
     }
 
     /**
@@ -1072,7 +1068,7 @@
                             runningTaskTarget.pictureInPictureParams) != null;
             if (mIsSwipingPipToHome) {
                 mSwipePipToHomeAnimator = getSwipePipToHomeAnimator(
-                        homeAnimFactory, runningTaskTarget);
+                        homeAnimFactory, runningTaskTarget, start);
                 mSwipePipToHomeAnimator.setDuration(SWIPE_PIP_TO_HOME_DURATION);
                 mSwipePipToHomeAnimator.setInterpolator(interpolator);
                 mSwipePipToHomeAnimator.setFloatValues(0f, 1f);
@@ -1142,23 +1138,34 @@
     }
 
     private SwipePipToHomeAnimator getSwipePipToHomeAnimator(HomeAnimationFactory homeAnimFactory,
-            RemoteAnimationTargetCompat runningTaskTarget) {
+            RemoteAnimationTargetCompat runningTaskTarget, float startProgress) {
         // Directly animate the app to PiP (picture-in-picture) mode
         final ActivityManager.RunningTaskInfo taskInfo = mGestureState.getRunningTask();
         final RecentsOrientedState orientationState = mTaskViewSimulator.getOrientationState();
+        final int windowRotation = orientationState.getDisplayRotation();
+        final int homeRotation = orientationState.getRecentsActivityRotation();
         final Rect destinationBounds = SystemUiProxy.INSTANCE.get(mContext)
                 .startSwipePipToHome(taskInfo.topActivity,
                         TaskInfoCompat.getTopActivityInfo(taskInfo),
                         runningTaskTarget.pictureInPictureParams,
-                        orientationState.getRecentsActivityRotation(),
+                        homeRotation,
                         mDp.hotseatBarSizePx);
+        final Rect startBounds = new Rect();
+        updateProgressForStartRect(new Matrix(), startProgress).round(startBounds);
         final SwipePipToHomeAnimator swipePipToHomeAnimator = new SwipePipToHomeAnimator(
                 runningTaskTarget.taskId,
                 taskInfo.topActivity,
                 runningTaskTarget.leash.getSurfaceControl(),
                 TaskInfoCompat.getPipSourceRectHint(runningTaskTarget.pictureInPictureParams),
                 TaskInfoCompat.getWindowConfigurationBounds(taskInfo),
+                startBounds,
                 destinationBounds);
+        // We would assume home and app window always in the same rotation While homeRotation
+        // is not ROTATION_0 (which implies the rotation is turned on in launcher settings).
+        if (homeRotation == ROTATION_0
+                && (windowRotation == ROTATION_90 || windowRotation == ROTATION_270)) {
+            swipePipToHomeAnimator.setFromRotation(mTaskViewSimulator, windowRotation);
+        }
         swipePipToHomeAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -1746,6 +1753,8 @@
             LiveTileOverlay.INSTANCE.update(
                     mTaskViewSimulator.getCurrentRect(),
                     mTaskViewSimulator.getCurrentCornerRadius());
+            LiveTileOverlay.INSTANCE.setRotation(
+                    mRecentsView.getPagedViewOrientedState().getDisplayRotation());
         }
         ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
     }
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index ffb05df..901040d 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -294,8 +294,12 @@
             if (mSurfaceControl != null) {
                 currentRect.roundOut(mTempRect);
                 Transaction t = new Transaction();
-                t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
-                t.apply();
+                try {
+                    t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+                    t.apply();
+                } catch (RuntimeException e) {
+                    // Ignore
+                }
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index 4bb1bb5..c0087b0 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -82,7 +82,6 @@
         mTaskViewSimulator.getOrientationState().update(
                 mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
                 mDeviceState.getRotationTouchHelper().getDisplayRotation());
-        mTaskViewSimulator.setDrawsBelowRecents(true);
 
         mMaxShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.max_shadow_radius);
         mTransformParams.setShadowRadius(mMaxShadowRadius);
@@ -176,6 +175,24 @@
     }
 
     /**
+     * Update with start progress for window animation to home.
+     * @param outMatrix {@link Matrix} to map a rect in Launcher space to window space.
+     * @param startProgress The progress of {@link #mCurrentShift} to start thw window from.
+     * @return {@link RectF} represents the bounds as starting point in window space.
+     */
+    protected RectF updateProgressForStartRect(Matrix outMatrix, float startProgress) {
+        mCurrentShift.updateValue(startProgress);
+        mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
+        RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
+
+        mTaskViewSimulator.applyWindowToHomeRotation(outMatrix);
+
+        final RectF startRect = new RectF(cropRectF);
+        mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
+        return startRect;
+    }
+
+    /**
      * Creates an animation that transforms the current app window into the home app.
      * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
      * @param homeAnimationFactory The home animation factory.
@@ -184,16 +201,11 @@
             HomeAnimationFactory homeAnimationFactory) {
         final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
 
-        mCurrentShift.updateValue(startProgress);
-        mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
+        Matrix homeToWindowPositionMap = new Matrix();
+        final RectF startRect = updateProgressForStartRect(
+                homeToWindowPositionMap, startProgress);
         RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
 
-        // Matrix to map a rect in Launcher space to window space
-        Matrix homeToWindowPositionMap = new Matrix();
-        mTaskViewSimulator.applyWindowToHomeRotation(homeToWindowPositionMap);
-
-        final RectF startRect = new RectF(cropRectF);
-        mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
         // Move the startRect to Launcher space as floatingIconView runs in Launcher
         Matrix windowToHomePositionMap = new Matrix();
         homeToWindowPositionMap.invert(windowToHomePositionMap);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 8ce1f51..1e0a00a 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -627,10 +627,6 @@
     }
 
     private void handleOrientationSetup(InputConsumer baseInputConsumer) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "handleOrientationSetup.1");
-        }
-
         baseInputConsumer.notifyOrientationSetup();
     }
 
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index 498e561..aad70c4 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
@@ -80,9 +79,6 @@
             ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR);
         }
         ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "OverviewInputConsumer");
-        }
         boolean handled = mTarget.proxyTouchEvent(ev, mStartingInActivityBounds);
         ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
         ev.setEdgeFlags(flags);
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index d1b0a70..a9a9e2a 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -56,7 +56,7 @@
 
 /** Utility class to handle Home and Assistant gestures. */
 public class NavBarGestureHandler implements OnTouchListener,
-        TriggerSwipeUpTouchTracker.OnSwipeUpListener {
+        TriggerSwipeUpTouchTracker.OnSwipeUpListener, MotionPauseDetector.OnMotionPauseListener {
 
     private static final String LOG_TAG = "NavBarGestureHandler";
     private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300;
@@ -181,7 +181,7 @@
                 mLaunchedAssistant = false;
                 mSwipeUpTouchTracker.init();
                 mMotionPauseDetector.clear();
-                mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
+                mMotionPauseDetector.setOnMotionPauseListener(this);
                 break;
             case MotionEvent.ACTION_MOVE:
                 mLastPos.set(event.getX(), event.getY());
@@ -256,7 +256,13 @@
                 || event.getY() >= mDisplaySize.y - mBottomGestureHeight;
     }
 
-    protected void onMotionPauseDetected() {
+    @Override
+    public void onMotionPauseChanged(boolean isPaused) {
+        mGestureCallback.onMotionPaused(isPaused);
+    }
+
+    @Override
+    public void onMotionPauseDetected() {
         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
     }
 
@@ -311,6 +317,9 @@
         /** Called whenever any touch is completed. */
         void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity);
 
+        /** Called when a motion stops or resumes */
+        default void onMotionPaused(boolean isPaused) {}
+
         /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
         default void setNavBarGestureProgress(@Nullable Float displacement) {}
 
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index 865b66e..68c63bf 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -61,16 +61,21 @@
 
 @TargetApi(Build.VERSION_CODES.R)
 abstract class SwipeUpGestureTutorialController extends TutorialController {
-    private final ViewSwipeUpAnimation mViewSwipeUpAnimation;
+
+    private static final int FAKE_PREVIOUS_TASK_MARGIN = Utilities.dpToPx(12);
+
+    private final ViewSwipeUpAnimation mTaskViewSwipeUpAnimation;
     private float mFakeTaskViewRadius;
     private Rect mFakeTaskViewRect = new Rect();
     private RunningWindowAnim mRunningWindowAnim;
+    private boolean mShowTasks = false;
+    private boolean mShowPreviousTasks = false;
 
     SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
         super(tutorialFragment, tutorialType);
         RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext);
         OverviewComponentObserver observer = new OverviewComponentObserver(mContext, deviceState);
-        mViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
+        mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
                 new GestureState(observer, -1));
         observer.onDestroy();
         deviceState.destroy();
@@ -83,16 +88,22 @@
                 .getWindowInsets()
                 .getInsets(WindowInsets.Type.systemBars());
         dp.updateInsets(new Rect(insets.left, insets.top, insets.right, insets.bottom));
-        mViewSwipeUpAnimation.initDp(dp);
+        mTaskViewSwipeUpAnimation.initDp(dp);
 
         mFakeTaskViewRadius = QuickStepContract.getWindowCornerRadius(mContext.getResources());
-        mFakeTaskView.setClipToOutline(true);
-        mFakeTaskView.setOutlineProvider(new ViewOutlineProvider() {
+
+        ViewOutlineProvider outlineProvider = new ViewOutlineProvider() {
             @Override
             public void getOutline(View view, Outline outline) {
                 outline.setRoundRect(mFakeTaskViewRect, mFakeTaskViewRadius);
             }
-        });
+        };
+
+        mFakeTaskView.setClipToOutline(true);
+        mFakeTaskView.setOutlineProvider(outlineProvider);
+
+        mFakePreviousTaskView.setClipToOutline(true);
+        mFakePreviousTaskView.setOutlineProvider(outlineProvider);
     }
 
     private void cancelRunningAnimation() {
@@ -114,16 +125,22 @@
                 mFakeIconView.setVisibility(View.INVISIBLE);
                 mFakeTaskView.setVisibility(View.INVISIBLE);
                 mFakeTaskView.setAlpha(1);
+                mFakePreviousTaskView.setVisibility(View.INVISIBLE);
+                mFakePreviousTaskView.setAlpha(1);
+                mShowTasks = false;
+                mShowPreviousTasks = false;
                 mRunningWindowAnim = null;
             }
         };
         if (toOverviewFirst) {
-            anim.setFloat(mViewSwipeUpAnimation.getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
+            anim.setFloat(mTaskViewSwipeUpAnimation
+                    .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation, boolean isReverse) {
                     PendingAnimation fadeAnim = new PendingAnimation(300);
                     fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+                    fadeAnim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
                     fadeAnim.addListener(resetTaskView);
                     AnimatorSet animset = fadeAnim.buildAnim();
                     animset.setStartDelay(100);
@@ -133,6 +150,7 @@
             });
         } else {
             anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+            anim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
             anim.setViewAlpha(mFakeIconView, 0, ACCEL);
             anim.addListener(resetTaskView);
         }
@@ -148,8 +166,10 @@
         hideFeedback();
         hideHandCoachingAnimation();
         cancelRunningAnimation();
+        mFakePreviousTaskView.setVisibility(View.INVISIBLE);
+        mShowPreviousTasks = false;
         RectFSpringAnim rectAnim =
-                mViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
+                mTaskViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
         // After home animation finishes, fade out and run onEndRunnable.
         rectAnim.addAnimatorListener(AnimationSuccessListener.forRunnable(
                 () -> fadeOutFakeTaskView(false, onEndRunnable)));
@@ -161,11 +181,31 @@
         if (displacement == null || mTutorialType == HOME_NAVIGATION_COMPLETE
                 || mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) {
             mFakeTaskView.setVisibility(View.INVISIBLE);
+            mFakePreviousTaskView.setVisibility(View.INVISIBLE);
         } else {
+            mShowTasks = true;
             mFakeTaskView.setVisibility(View.VISIBLE);
-            if (mRunningWindowAnim == null) {
-                mViewSwipeUpAnimation.updateDisplacement(displacement);
+            if (mShowPreviousTasks) {
+                mFakePreviousTaskView.setVisibility(View.VISIBLE);
             }
+            if (mRunningWindowAnim == null) {
+                mTaskViewSwipeUpAnimation.updateDisplacement(displacement);
+            }
+        }
+    }
+
+    @Override
+    public void onMotionPaused(boolean unused) {
+        if (mShowTasks) {
+            if (!mShowPreviousTasks) {
+                mFakePreviousTaskView.setTranslationX(
+                        -(2 * mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN));
+                mFakePreviousTaskView.animate()
+                    .setDuration(300)
+                    .translationX(-(mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN))
+                    .start();
+            }
+            mShowPreviousTasks = true;
         }
     }
 
@@ -232,6 +272,7 @@
                             false /* isVerticalBarLayout */);
                     mFakeIconView.setAlpha(1);
                     mFakeTaskView.setAlpha(getWindowAlpha(progress));
+                    mFakePreviousTaskView.setAlpha(getWindowAlpha(progress));
                 }
 
                 @Override
@@ -258,9 +299,11 @@
         public void applySurfaceParams(SurfaceParams[] params) {
             SurfaceParams p = params[0];
             mFakeTaskView.setAnimationMatrix(p.matrix);
+            mFakePreviousTaskView.setAnimationMatrix(p.matrix);
             mFakeTaskViewRect.set(p.windowCrop);
             mFakeTaskViewRadius = p.cornerRadius;
             mFakeTaskView.invalidateOutline();
+            mFakePreviousTaskView.invalidateOutline();
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 0d5a110..12d2efc 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -52,6 +52,7 @@
     final View mLauncherView;
     final ClipIconView mFakeIconView;
     final View mFakeTaskView;
+    final View mFakePreviousTaskView;
     final View mRippleView;
     final RippleDrawable mRippleDrawable;
     @Nullable final TutorialHandAnimation mHandCoachingAnimation;
@@ -74,6 +75,8 @@
         mLauncherView = getMockLauncherView();
         mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
+        mFakePreviousTaskView =
+                rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view);
         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
         mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
         mHandCoachingAnimation = tutorialFragment.getHandAnimation();
@@ -93,6 +96,8 @@
         if (mContext != null) {
             rootView.setBackground(mContext.getDrawable(getMockWallpaperResId()));
             mFakeTaskView.setBackground(mContext.getDrawable(getMockAppTaskThumbnailResId()));
+            mFakePreviousTaskView.setBackground(
+                    mContext.getDrawable(getMockPreviousAppTaskThumbnailResId()));
             mFakeIconView.setBackground(mContext.getDrawable(getMockAppIconResId()));
         }
     }
@@ -126,6 +131,11 @@
         return R.drawable.default_sandbox_app_task_thumbnail;
     }
 
+    @DrawableRes
+    protected int getMockPreviousAppTaskThumbnailResId() {
+        return R.drawable.default_sandbox_app_previous_task_thumbnail;
+    }
+
     @Nullable
     public View getMockLauncherView() {
         InvariantDeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext);
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index da5f59e..8cde5f2 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -19,14 +19,12 @@
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 
 import com.android.launcher3.Alarm;
 import com.android.launcher3.R;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
 
 /**
  * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is
@@ -87,9 +85,6 @@
         mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
         mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
         mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "creating alarm");
-        }
         mForcePauseTimeout = new Alarm();
         mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
         mMakePauseHarderToTrigger = makePauseHarderToTrigger;
@@ -126,9 +121,6 @@
      * @param pointerIndex Index for the pointer being tracked in the motion event
      */
     public void addPosition(MotionEvent ev, int pointerIndex) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "setting alarm");
-        }
         mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger
                 ? HARDER_TRIGGER_TIMEOUT
                 : FORCE_PAUSE_TIMEOUT);
@@ -176,9 +168,6 @@
     }
 
     private void updatePaused(boolean isPaused) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "updatePaused: " + isPaused);
-        }
         if (mDisallowPause) {
             isPaused = false;
         }
@@ -207,9 +196,6 @@
         setOnMotionPauseListener(null);
         mIsPaused = mHasEverBeenPaused = false;
         mSlowStartTime = 0;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "canceling alarm");
-        }
         mForcePauseTimeout.cancelAlarm();
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index e273aeb..facc99a 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -519,6 +519,29 @@
         }
     }
 
+    /**
+     * Contrary to {@link #postDisplayRotation}.
+     */
+    public static void preDisplayRotation(@SurfaceRotation int displayRotation,
+            float screenWidth, float screenHeight, Matrix out) {
+        switch (displayRotation) {
+            case ROTATION_0:
+                return;
+            case ROTATION_90:
+                out.postRotate(90);
+                out.postTranslate(screenWidth, 0);
+                break;
+            case ROTATION_180:
+                out.postRotate(180);
+                out.postTranslate(screenHeight, screenWidth);
+                break;
+            case ROTATION_270:
+                out.postRotate(270);
+                out.postTranslate(0, screenHeight);
+                break;
+        }
+    }
+
     @NonNull
     @Override
     public String toString() {
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 87fee79..8fbd645 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -16,17 +16,24 @@
 
 package com.android.quickstep.util;
 
+import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_PIP;
+
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.RectEvaluator;
 import android.animation.ValueAnimator;
 import android.content.ComponentName;
+import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.Surface;
 import android.view.SurfaceControl;
 
 import androidx.annotation.NonNull;
 
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 /**
  * An {@link Animator} that animates an Activity to PiP (picture-in-picture) window when
@@ -39,9 +46,12 @@
  */
 public class SwipePipToHomeAnimator extends ValueAnimator implements
         ValueAnimator.AnimatorUpdateListener {
+    private static final String TAG = SwipePipToHomeAnimator.class.getSimpleName();
+
     private final int mTaskId;
     private final ComponentName mComponentName;
     private final SurfaceControl mLeash;
+    private final Rect mAppBounds = new Rect();
     private final Rect mStartBounds = new Rect();
     private final Rect mDestinationBounds = new Rect();
     private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
@@ -52,49 +62,134 @@
     private final Rect mSourceHintRectInsets = new Rect();
     private final Rect mSourceInsets = new Rect();
 
+    /** for rotation via {@link #setFromRotation(TaskViewSimulator, int)} */
+    private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0;
+    private final Rect mDestinationBoundsTransformed = new Rect();
+    private final Rect mDestinationBoundsAnimation = new Rect();
+
     /**
      * Flag to avoid the double-end problem since the leash would have been released
      * after the first end call and any further operations upon it would lead to NPE.
      */
     private boolean mHasAnimationEnded;
 
+    /**
+     * @param taskId Task id associated with this animator, see also {@link #getTaskId()}
+     * @param componentName Component associated with this animator,
+     *                      see also {@link #getComponentName()}
+     * @param leash {@link SurfaceControl} this animator operates on
+     * @param sourceRectHint See the definition in {@link android.app.PictureInPictureParams}
+     * @param appBounds Bounds of the application, sourceRectHint is based on this bounds
+     * @param startBounds Bounds of the application when this animator starts. This can be
+     *                    different from the appBounds if user has swiped a certain distance and
+     *                    Launcher has performed transform on the leash.
+     * @param destinationBounds Bounds of the destination this animator ends to
+     */
     public SwipePipToHomeAnimator(int taskId,
             @NonNull ComponentName componentName,
             @NonNull SurfaceControl leash,
             @NonNull Rect sourceRectHint,
+            @NonNull Rect appBounds,
             @NonNull Rect startBounds,
             @NonNull Rect destinationBounds) {
         mTaskId = taskId;
         mComponentName = componentName;
         mLeash = leash;
+        mAppBounds.set(appBounds);
         mStartBounds.set(startBounds);
         mDestinationBounds.set(destinationBounds);
+        mDestinationBoundsTransformed.set(mDestinationBounds);
+        mDestinationBoundsAnimation.set(mDestinationBounds);
         mSurfaceTransactionHelper = new PipSurfaceTransactionHelper();
 
-        mSourceHintRectInsets.set(sourceRectHint.left - startBounds.left,
-                sourceRectHint.top - startBounds.top,
-                startBounds.right - sourceRectHint.right,
-                startBounds.bottom - sourceRectHint.bottom);
+        mSourceHintRectInsets.set(sourceRectHint.left - appBounds.left,
+                sourceRectHint.top - appBounds.top,
+                appBounds.right - sourceRectHint.right,
+                appBounds.bottom - sourceRectHint.bottom);
 
-        addListener(new AnimatorListenerAdapter() {
+        addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                InteractionJankMonitorWrapper.begin(CUJ_APP_CLOSE_TO_PIP);
+                super.onAnimationStart(animation);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                super.onAnimationCancel(animation);
+                InteractionJankMonitorWrapper.cancel(CUJ_APP_CLOSE_TO_PIP);
+            }
+
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                InteractionJankMonitorWrapper.end(CUJ_APP_CLOSE_TO_PIP);
+            }
+
             @Override
             public void onAnimationEnd(Animator animation) {
+                if (!mHasAnimationEnded) super.onAnimationEnd(animation);
                 SwipePipToHomeAnimator.this.onAnimationEnd();
             }
         });
         addUpdateListener(this);
     }
 
+    /** sets the from rotation if it's different from the target rotation. */
+    public void setFromRotation(TaskViewSimulator taskViewSimulator,
+            @RecentsOrientedState.SurfaceRotation int fromRotation) {
+        if (fromRotation != Surface.ROTATION_90 && fromRotation != Surface.ROTATION_270) {
+            Log.wtf(TAG, "Not a supported rotation, rotation=" + fromRotation);
+            return;
+        }
+        mFromRotation = fromRotation;
+        final Matrix matrix = new Matrix();
+        taskViewSimulator.applyWindowToHomeRotation(matrix);
+
+        // map the destination bounds into window space. mDestinationBounds is always calculated
+        // in the final home space and the animation runs in original window space.
+        final RectF transformed = new RectF(mDestinationBounds);
+        matrix.mapRect(transformed, new RectF(mDestinationBounds));
+        transformed.round(mDestinationBoundsTransformed);
+
+        // set the animation destination bounds for RectEvaluator calculation.
+        // bounds and insets are calculated as if the transition is from mAppBounds to
+        // mDestinationBoundsAnimation, separated from rotate / scale / position.
+        mDestinationBoundsAnimation.set(mAppBounds.left, mAppBounds.top,
+                mAppBounds.left + mDestinationBounds.width(),
+                mAppBounds.top + mDestinationBounds.height());
+    }
+
     @Override
     public void onAnimationUpdate(ValueAnimator animator) {
         if (mHasAnimationEnded) return;
 
         final float fraction = animator.getAnimatedFraction();
-        final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds, mDestinationBounds);
+        final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds,
+                mDestinationBoundsAnimation);
         final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
                 mSourceHintRectInsets);
-        final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-        mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mStartBounds, bounds, insets);
+        final SurfaceControl.Transaction tx =
+                PipSurfaceTransactionHelper.newSurfaceControlTransaction();
+        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+            final float degree, positionX, positionY;
+            if (mFromRotation == Surface.ROTATION_90) {
+                degree = -90 * fraction;
+                positionX = fraction * (mDestinationBoundsTransformed.left - mAppBounds.left)
+                        + mAppBounds.left;
+                positionY = fraction * (mDestinationBoundsTransformed.bottom - mAppBounds.top)
+                        + mAppBounds.top;
+            } else {
+                degree = 90 * fraction;
+                positionX = fraction * (mDestinationBoundsTransformed.right - mAppBounds.left)
+                        + mAppBounds.left;
+                positionY = fraction * (mDestinationBoundsTransformed.top - mAppBounds.top)
+                        + mAppBounds.top;
+            }
+            mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
+                    degree, positionX, positionY);
+        } else {
+            mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mAppBounds, bounds, insets);
+        }
         mSurfaceTransactionHelper.resetCornerRadius(tx, mLeash);
         tx.apply();
     }
@@ -114,8 +209,9 @@
     private void onAnimationEnd() {
         if (mHasAnimationEnded) return;
 
-        final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-        mSurfaceTransactionHelper.reset(tx, mLeash, mDestinationBounds);
+        final SurfaceControl.Transaction tx =
+                PipSurfaceTransactionHelper.newSurfaceControlTransaction();
+        mSurfaceTransactionHelper.reset(tx, mLeash, mDestinationBoundsTransformed, mFromRotation);
         tx.apply();
         mHasAnimationEnded = true;
     }
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 2f4bb8e..5a7f541 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.states.RotationHelper.deltaRotation;
 import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
+import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
 import android.animation.TimeInterpolator;
@@ -80,6 +81,7 @@
     private DeviceProfile mDp;
 
     private final Matrix mMatrix = new Matrix();
+    private final Matrix mMatrixTmp = new Matrix();
     private final Point mRunningTargetWindowPosition = new Point();
 
     // Thumbnail view properties
@@ -211,7 +213,10 @@
      */
     public RectF getCurrentRect() {
         RectF result = getCurrentCropRect();
-        mMatrix.mapRect(result);
+        mMatrixTmp.set(mMatrix);
+        preDisplayRotation(mOrientationState.getDisplayRotation(), mDp.widthPx, mDp.heightPx,
+                mMatrixTmp);
+        mMatrixTmp.mapRect(result);
         return result;
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java
index f6eb0e2..747c3f2 100644
--- a/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java
+++ b/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java
@@ -1,5 +1,10 @@
 package com.android.quickstep.views;
 
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 
@@ -19,6 +24,7 @@
 import android.view.ViewOverlay;
 
 import com.android.launcher3.anim.Interpolators;
+import com.android.quickstep.util.RecentsOrientedState.SurfaceRotation;
 
 public class LiveTileOverlay extends Drawable {
 
@@ -43,6 +49,8 @@
     private final RectF mCurrentRect = new RectF();
     private final Rect mBoundsRect = new Rect();
 
+    private @SurfaceRotation int mRotation = ROTATION_0;
+
     private float mCornerRadius;
     private Drawable mIcon;
     private Animator mIconAnimator;
@@ -69,6 +77,10 @@
         mCurrentRect.set(left, top, right, bottom);
     }
 
+    public void setRotation(@SurfaceRotation int rotation) {
+        mRotation = rotation;
+    }
+
     public void setIcon(Drawable icon) {
         mIcon = icon;
     }
@@ -103,8 +115,35 @@
             canvas.save();
             float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, 0f,
                     1f).getInterpolation(mIconAnimationProgress);
-            canvas.translate(mCurrentRect.centerX() - mIcon.getBounds().width() / 2 * scale,
-                    mCurrentRect.top - mIcon.getBounds().height() / 2 * scale);
+
+            int iconRadius = mIcon.getBounds().width() / 2;
+            float dx = 0;
+            float dy = 0;
+
+            switch (mRotation) {
+                case ROTATION_0:
+                    dx = mCurrentRect.centerX() - iconRadius * scale;
+                    dy = mCurrentRect.top - iconRadius * scale;
+                    break;
+                case ROTATION_90:
+                    dx = mCurrentRect.right - iconRadius * scale;
+                    dy = mCurrentRect.centerY() - iconRadius * scale;
+                    break;
+                case ROTATION_270:
+                    dx = mCurrentRect.left - iconRadius * scale;
+                    dy = mCurrentRect.centerY() - iconRadius * scale;
+                    break;
+                case ROTATION_180:
+                    dx = mCurrentRect.centerX() - iconRadius * scale;
+                    dy = mCurrentRect.bottom - iconRadius * scale;
+                    break;
+            }
+
+            int rotationDegrees = mRotation * 90;
+            if (mRotation == ROTATION_90 || mRotation == ROTATION_270) {
+                canvas.rotate(rotationDegrees, dx + iconRadius, dy + iconRadius);
+            }
+            canvas.translate(dx, dy);
             canvas.scale(scale, scale);
             mIcon.draw(canvas);
             canvas.restore();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index c5863c1..cc97f4b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -310,6 +310,10 @@
             assertTrue("The second app we should have quick switched to is not running",
                     isTestActivityRunning(2));
         }
+        background = getAndAssertBackground();
+        background.quickSwitchToPreviousAppSwipeLeft();
+        assertTrue("The 2nd app we should have quick switched to is not running",
+                isTestActivityRunning(3));
         getAndAssertBackground();
     }
 
diff --git a/res/values/colors.xml b/res/values/colors.xml
index f56fbaa..78c2df6 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -37,6 +37,7 @@
 
     <color name="gesture_tutorial_ripple_color">#A0C2F9</color> <!-- Light Blue -->
     <color name="gesture_tutorial_fake_task_view_color">#6DA1FF</color> <!-- Light Blue -->
+    <color name="gesture_tutorial_fake_previous_task_view_color">#9CCC65</color> <!-- Light Green -->
     <color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
     <color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
 </resources>
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index ff53b5f..1d88e83 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.Utilities.getDevicePrefs;
 import static com.android.launcher3.Utilities.getPointString;
 import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
@@ -215,6 +216,9 @@
     }
 
     public static String getCurrentGridName(Context context) {
+        if (ENABLE_FOUR_COLUMNS.get()) {
+            return ENABLE_FOUR_COLUMNS.key;
+        }
         return Utilities.isGridOptionsEnabled(context)
                 ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null;
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index ea98157..e099d85 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -961,8 +961,6 @@
         DiscoveryBounce.showForHomeIfNeeded(this);
     }
 
-    protected void handlePendingActivityRequest() { }
-
     private void logStopAndResume(boolean isResume) {
         if (mPendingExecutor != null) return;
         int pageIndex = mWorkspace.isOverlayShown() ? -1 : mWorkspace.getCurrentPage();
@@ -1053,7 +1051,7 @@
 
     @Override
     public void onStateSetEnd(LauncherState state) {
-        super.onStateSetStart(state);
+        super.onStateSetEnd(state);
         getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
         getWorkspace().setClipChildren(!state.hasFlag(FLAG_MULTI_PAGE));
 
@@ -1479,8 +1477,7 @@
                 if (!isInState(NORMAL)) {
                     // Only change state, if not already the same. This prevents cancelling any
                     // animations running as part of resume
-                    mStateManager.goToState(NORMAL, mStateManager.shouldAnimateStateChange(),
-                            this::handlePendingActivityRequest);
+                    mStateManager.goToState(NORMAL, mStateManager.shouldAnimateStateChange());
                 }
 
                 // Reset the apps view
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index aeed16a..e151777 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -1099,11 +1099,20 @@
      * @return the max _id in the provided table.
      */
     @Thunk static int getMaxId(SQLiteDatabase db, String query, Object... args) {
-        int max = (int) DatabaseUtils.longForQuery(db,
-                String.format(Locale.ENGLISH, query, args),
-                null);
-        if (max < 0) {
-            throw new RuntimeException("Error: could not query max id");
+        int max = 0;
+        try (SQLiteStatement prog = db.compileStatement(
+                String.format(Locale.ENGLISH, query, args))) {
+            max = (int) DatabaseUtils.longForQuery(prog, null);
+            if (max < 0) {
+                throw new RuntimeException("Error: could not query max id");
+            }
+        } catch (IllegalArgumentException exception) {
+            String message = exception.getMessage();
+            if (message.contains("re-open") && message.contains("already-closed")) {
+                // Don't crash trying to end a transaction an an already closed DB. See b/173162852.
+            } else {
+                throw exception;
+            }
         }
         return max;
     }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 1e023df..5c2f35b 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -404,6 +404,11 @@
         return (size / densityRatio);
     }
 
+    /** Converts a dp value to pixels for the current device. */
+    public static int dpToPx(float dp) {
+        return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
+    }
+
     public static int pxFromSp(float size, DisplayMetrics metrics) {
         return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                 size, metrics));
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index edaf51d..f6d1651 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -29,6 +29,8 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 
+import com.android.launcher3.Utilities;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -133,15 +135,20 @@
 
     /**
      * Starts playing the animation with the provided velocity optionally playing any
-     * physics based animations
+     * physics based animations.
+     * @param goingToEnd Whether we are going to the end (progress = 1) or not (progress = 0).
+     * @param velocityPxPerMs The velocity at which to start the animation, in pixels / millisecond.
+     * @param endDistance The distance (pixels) that the animation will travel from progress 0 to 1.
+     * @param animationDuration The duration of the non-physics based animation.
      */
     public void startWithVelocity(Context context, boolean goingToEnd,
-            float velocity, float scale, long animationDuration) {
-        float scaleInverse = 1 / Math.abs(scale);
-        float scaledVelocity = velocity * scaleInverse;
+            float velocityPxPerMs, float endDistance, long animationDuration) {
+        float distanceInverse = 1 / Math.abs(endDistance);
+        float velocityProgressPerMs = velocityPxPerMs * distanceInverse;
 
+        float oneFrameProgress = velocityProgressPerMs * getSingleFrameMs(context);
         float nextFrameProgress = boundToRange(getProgressFraction()
-                + scaledVelocity * getSingleFrameMs(context), 0f, 1f);
+                + oneFrameProgress, 0f, 1f);
 
         // Update setters for spring
         int springFlag = goingToEnd
@@ -154,8 +161,8 @@
                 SpringAnimationBuilder s = new SpringAnimationBuilder(context)
                         .setStartValue(mCurrentFraction)
                         .setEndValue(goingToEnd ? 1 : 0)
-                        .setStartVelocity(scaledVelocity)
-                        .setMinimumVisibleChange(scaleInverse)
+                        .setStartVelocity(velocityProgressPerMs)
+                        .setMinimumVisibleChange(distanceInverse)
                         .setDampingRatio(h.springProperty.mDampingRatio)
                         .setStiffness(h.springProperty.mStiffness)
                         .computeParams();
@@ -164,8 +171,18 @@
                 springDuration = Math.max(expectedDurationL, springDuration);
 
                 float expectedDuration = expectedDurationL;
-                h.mapper = (progress, globalEndProgress) ->
-                        mAnimationPlayer.getCurrentPlayTime() / expectedDuration;
+                h.mapper = (progress, globalEndProgress) -> {
+                    if (expectedDuration <= 0 || oneFrameProgress >= 1) {
+                        return 1;
+                    } else {
+                        // Start from one frame ahead of the current position.
+                        return Utilities.mapToRange(
+                                mAnimationPlayer.getCurrentPlayTime() / expectedDuration,
+                                0, 1,
+                                Math.abs(oneFrameProgress), 1,
+                                LINEAR);
+                    }
+                };
                 h.anim.setInterpolator(s::getInterpolatedValue);
             }
         }
@@ -174,7 +191,7 @@
 
         if (springDuration <= animationDuration) {
             mAnimationPlayer.setDuration(animationDuration);
-            mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocity));
+            mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocityPxPerMs));
         } else {
             // Since spring requires more time to run, we let the other animations play with
             // current time and interpolation and by clamping the duration.
@@ -182,7 +199,7 @@
 
             float cutOff = animationDuration / (float) springDuration;
             mAnimationPlayer.setInterpolator(
-                    clampToProgress(scrollInterpolatorForVelocity(velocity), 0, cutOff));
+                    clampToProgress(scrollInterpolatorForVelocity(velocityPxPerMs), 0, cutOff));
         }
         mAnimationPlayer.start();
     }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 8f63c09..883eab0 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -190,6 +190,10 @@
             "EXPANDED_SMARTSPACE", false, "Expands smartspace height to two rows. "
               + "Any apps occupying the first row will be removed from workspace.");
 
+    public static final BooleanFlag ENABLE_FOUR_COLUMNS = new DeviceFlag(
+            "ENABLE_FOUR_COLUMNS", false, "Uses 4 columns in launcher grid."
+            + "Warning: This will permanently alter your home screen items and is not reversible.");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index effb3a4..efc1201 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -629,11 +629,13 @@
         private WorkspaceResult(BgDataModel dataModel,
                 WidgetsModel widgetsModel,
                 Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
-            mWorkspaceItems = dataModel.workspaceItems;
-            mAppWidgets = dataModel.appWidgets;
-            mHotseatPredictions = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
-            mWidgetsModel = widgetsModel;
-            mWidgetProvidersMap = widgetProviderInfoMap;
+            synchronized (dataModel) {
+                mWorkspaceItems = dataModel.workspaceItems;
+                mAppWidgets = dataModel.appWidgets;
+                mHotseatPredictions = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
+                mWidgetsModel = widgetsModel;
+                mWidgetProvidersMap = widgetProviderInfoMap;
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 49b40ed..c217a47 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -376,6 +376,16 @@
     }
 
     /**
+     * Returns a list containing all workspace items including widgets.
+     */
+    public synchronized ArrayList<ItemInfo> getAllWorkspaceItems() {
+        ArrayList<ItemInfo> items = new ArrayList<>(workspaceItems.size() + appWidgets.size());
+        items.addAll(workspaceItems);
+        items.addAll(appWidgets);
+        return items;
+    }
+
+    /**
      * Calls the provided {@code op} for all workspaceItems in the in-memory model (both persisted
      * items and dynamic/predicted items for the provided {@code userHandle}.
      * Note the call is not synchronized over the model, that should be handled by the called.
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index b108788..8e085ce 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -166,12 +166,7 @@
 
     private void sendFirstScreenActiveInstallsBroadcast() {
         ArrayList<ItemInfo> firstScreenItems = new ArrayList<>();
-
-        ArrayList<ItemInfo> allItems = new ArrayList<>();
-        synchronized (mBgDataModel) {
-            allItems.addAll(mBgDataModel.workspaceItems);
-            allItems.addAll(mBgDataModel.appWidgets);
-        }
+        ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
         // Screen set is never empty
         final int firstScreen = mBgDataModel.collectWorkspaceScreens().get(0);
 
@@ -858,10 +853,12 @@
                     .call(contentResolver,
                             LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS)
                     .getIntArray(LauncherSettings.Settings.EXTRA_VALUE);
-            for (int folderId : deletedFolderIds) {
-                mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId));
-                mBgDataModel.folders.remove(folderId);
-                mBgDataModel.itemsIdMap.remove(folderId);
+            synchronized (mBgDataModel) {
+                for (int folderId : deletedFolderIds) {
+                    mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId));
+                    mBgDataModel.folders.remove(folderId);
+                    mBgDataModel.itemsIdMap.remove(folderId);
+                }
             }
 
             // Remove any ghost widgets
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index b2d0081..218172b 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -103,7 +103,6 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-    public static final String PAUSE_NOT_DETECTED = "b/139891609";
     public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
     public static final String NO_SWIPE_TO_HOME = "b/158017601";
     public static final String WORK_PROFILE_REMOVED = "b/159671700";
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 7c02f3d..23baaf0 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -172,9 +172,6 @@
 
     @Override
     public final boolean onControllerTouchEvent(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "onControllerTouchEvent");
-        }
         return mDetector.onTouchEvent(ev);
     }
 
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 01b33d8..1276ece 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -26,8 +26,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.testing.TestProtocol;
-
 import java.util.LinkedList;
 import java.util.Queue;
 
@@ -175,9 +173,6 @@
                 if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {
                     setState(ScrollState.DRAGGING);
                 }
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.PAUSE_NOT_DETECTED, "before report dragging");
-                }
                 if (mState == ScrollState.DRAGGING) {
                     reportDragging(ev);
                 }
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 1d7f747..f05f15e 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -75,11 +75,6 @@
     }
 
     @Override
-    public boolean isGoingUp(float displacement, boolean isRtl) {
-        return isRtl ? displacement < 0 : displacement > 0;
-    }
-
-    @Override
     public boolean isLayoutNaturalToLauncher() {
         return false;
     }
@@ -215,11 +210,6 @@
     }
 
     @Override
-    public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
-        return HORIZONTAL;
-    }
-
-    @Override
     public int getPrimaryTranslationDirectionFactor() {
         return -1;
     }
@@ -229,11 +219,6 @@
     }
 
     @Override
-    public int getTaskDragDisplacementFactor(boolean isRtl) {
-        return isRtl ? 1 : -1;
-    }
-
-    @Override
     public float getTaskMenuX(float x, View thumbnailView) {
         return thumbnailView.getMeasuredWidth() + x;
     }
@@ -261,6 +246,31 @@
         lp.weight = 1;
     }
 
+    /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+
+    @Override
+    public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
+        return HORIZONTAL;
+    }
+
+    @Override
+    public int getUpDirection(boolean isRtl) {
+        return isRtl ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+                : SingleAxisSwipeDetector.DIRECTION_POSITIVE;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        return isRtl ? displacement < 0 : displacement > 0;
+    }
+
+    @Override
+    public int getTaskDragDisplacementFactor(boolean isRtl) {
+        return isRtl ? 1 : -1;
+    }
+
+    /* -------------------- */
+
     @Override
     public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
         boolean layoutChild) {
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index a9c50cd..b9acfa3 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -75,10 +75,8 @@
     int getCenterForPage(View view, Rect insets);
     int getScrollOffsetStart(View view, Rect insets);
     int getScrollOffsetEnd(View view, Rect insets);
-    SingleAxisSwipeDetector.Direction getOppositeSwipeDirection();
     int getPrimaryTranslationDirectionFactor();
     int getSecondaryTranslationDirectionFactor();
-    int getTaskDragDisplacementFactor(boolean isRtl);
     ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
     void setMaxScroll(AccessibilityEvent event, int maxScroll);
     boolean getRecentsRtlSetting(Resources resources);
@@ -92,7 +90,6 @@
     void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y);
     void scrollerStartScroll(OverScroller scroller, int newPosition);
     void getCurveProperties(PagedView view, Rect insets, CurveProperties out);
-    boolean isGoingUp(float displacement, boolean isRtl);
     boolean isLayoutNaturalToLauncher();
     float getTaskMenuX(float x, View thumbnailView);
     float getTaskMenuY(float y, View thumbnailView);
@@ -101,6 +98,16 @@
     void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
     int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
 
+    // The following are only used by TaskViewTouchHandler.
+    /** @return Either VERTICAL or HORIZONTAL. */
+    SingleAxisSwipeDetector.Direction getUpDownSwipeDirection();
+    /** @return Given {@link #getUpDownSwipeDirection()}, whether POSITIVE or NEGATIVE is up. */
+    int getUpDirection(boolean isRtl);
+    /** @return Whether the displacement is going towards the top of the screen. */
+    boolean isGoingUp(float displacement, boolean isRtl);
+    /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
+    int getTaskDragDisplacementFactor(boolean isRtl);
+
     /**
      * Maps the velocity from the coordinate plane of the foreground app to that
      * of Launcher's (which now will always be portrait)
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 587e35a..3663b5f 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -73,12 +73,6 @@
     }
 
     @Override
-    public boolean isGoingUp(float displacement, boolean isRtl) {
-        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
-        return displacement < 0;
-    }
-
-    @Override
     public boolean isLayoutNaturalToLauncher() {
         return true;
     }
@@ -212,11 +206,6 @@
     }
 
     @Override
-    public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
-        return VERTICAL;
-    }
-
-    @Override
     public int getPrimaryTranslationDirectionFactor() {
         return 1;
     }
@@ -226,12 +215,6 @@
     }
 
     @Override
-    public int getTaskDragDisplacementFactor(boolean isRtl) {
-        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
-        return 1;
-    }
-
-    @Override
     public float getTaskMenuX(float x, View thumbnailView) {
         return x;
     }
@@ -257,6 +240,33 @@
         // no-op, defaults are fine
     }
 
+    /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+
+    @Override
+    public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
+        return VERTICAL;
+    }
+
+    @Override
+    public int getUpDirection(boolean isRtl) {
+        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+        return SingleAxisSwipeDetector.DIRECTION_POSITIVE;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+        return displacement < 0;
+    }
+
+    @Override
+    public int getTaskDragDisplacementFactor(boolean isRtl) {
+        // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
+        return 1;
+    }
+
+    /* -------------------- */
+
     @Override
     public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
         boolean layoutChild) {
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index 60d19d9..54af029 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.touch;
 
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+
 import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -33,11 +35,6 @@
     }
 
     @Override
-    public int getTaskDragDisplacementFactor(boolean isRtl) {
-        return isRtl ? -1 : 1;
-    }
-
-    @Override
     public boolean getRecentsRtlSetting(Resources resources) {
         return Utilities.isRtl(resources);
     }
@@ -53,11 +50,6 @@
     }
 
     @Override
-    public boolean isGoingUp(float displacement, boolean isRtl) {
-        return isRtl ? displacement > 0 : displacement < 0;
-    }
-
-    @Override
     public void adjustFloatingIconStartVelocity(PointF velocity) {
         float oldX = velocity.x;
         float oldY = velocity.y;
@@ -78,4 +70,29 @@
     public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
         return dp.widthPx - rect.right;
     }
+
+    /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+
+    @Override
+    public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
+        return HORIZONTAL;
+    }
+
+    @Override
+    public int getUpDirection(boolean isRtl) {
+        return isRtl ? SingleAxisSwipeDetector.DIRECTION_POSITIVE
+                : SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement, boolean isRtl) {
+        return isRtl ? displacement > 0 : displacement < 0;
+    }
+
+    @Override
+    public int getTaskDragDisplacementFactor(boolean isRtl) {
+        return isRtl ? -1 : 1;
+    }
+
+    /* -------------------- */
 }
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index 875eefb..8c3c115 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -17,7 +17,6 @@
 
 import android.content.Context;
 import android.graphics.PointF;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
@@ -25,7 +24,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
 
 /**
  * One dimensional scroll/drag/swipe gesture detector (either HORIZONTAL or VERTICAL).
@@ -60,6 +58,11 @@
             return direction.x;
         }
 
+        @NonNull
+        @Override
+        public String toString() {
+            return "VERTICAL";
+        }
     };
 
     public static final Direction HORIZONTAL = new Direction() {
@@ -86,6 +89,11 @@
             return direction.y;
         }
 
+        @NonNull
+        @Override
+        public String toString() {
+            return "HORIZONTAL";
+        }
     };
 
     private final Direction mDir;
@@ -105,11 +113,6 @@
         super(config, isRtl);
         mListener = l;
         mDir = dir;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector.ctor "
-                    + l.getClass().getSimpleName()
-                    + " @ " + android.util.Log.getStackTraceString(new Throwable()));
-        }
     }
 
     public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
@@ -117,10 +120,6 @@
         mIgnoreSlopWhenSettling = ignoreSlop;
     }
 
-    public int getScrollDirections() {
-        return mScrollDirections;
-    }
-
     /**
      * Returns if the start drag was towards the positive direction or negative.
      *
@@ -161,10 +160,6 @@
 
     @Override
     protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "SingleAxisSwipeDetector "
-                    + mListener.getClass().getSimpleName());
-        }
         mListener.onDrag(mDir.extractDirection(displacement),
                 mDir.extractOrthogonalDirection(displacement), event);
     }
diff --git a/src/com/android/launcher3/util/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java
index 4b22429..9fe72ad 100644
--- a/src/com/android/launcher3/util/SecureSettingsObserver.java
+++ b/src/com/android/launcher3/util/SecureSettingsObserver.java
@@ -88,7 +88,7 @@
     public static SecureSettingsObserver newOneHandedSettingsObserver(Context context,
             OnChangeListener listener) {
         return new SecureSettingsObserver(
-                context.getContentResolver(), listener, ONE_HANDED_ENABLED, 1);
+                context.getContentResolver(), listener, ONE_HANDED_ENABLED, 0);
     }
 
     /**
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 2be827b..5464dd8 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -33,7 +33,6 @@
 import android.graphics.RectF;
 import android.os.Build;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.Property;
 import android.view.MotionEvent;
 import android.view.View;
@@ -49,7 +48,6 @@
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -191,12 +189,6 @@
     }
 
     private TouchController findControllerToHandleTouch(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "findControllerToHandleTouch ev=" + ev
-                    + ", isEventInLauncher=" + isEventInLauncher(ev)
-                    + ", topOpenView=" + AbstractFloatingView.getTopOpenView(mActivity));
-        }
-
         AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
         if (topView != null
                 && (isEventInLauncher(ev) || topView.canInterceptEventsInSystemGestureRegion())
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index d9a14e9..1857c5a 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -55,7 +55,6 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.testing.TestProtocol;
 
 /**
  * A view that is created to look like another view with the purpose of creating fluid animations.
@@ -534,11 +533,6 @@
         view.setVisibility(INVISIBLE);
         parent.addView(view);
         dragLayer.addView(view.mListenerView);
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "getFloatingIconView. listenerView "
-                    + "added to dragLayer. listenerView=" + view.mListenerView + ", fiv=" + view,
-                    new Exception());
-        }
         view.mListenerView.setListener(view::fastFinish);
 
         view.mEndRunnable = () -> {
@@ -578,10 +572,6 @@
     private void finish(DragLayer dragLayer) {
         ((ViewGroup) dragLayer.getParent()).removeView(this);
         dragLayer.removeView(mListenerView);
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "listenerView removed from dragLayer. "
-                    + "listenerView=" + mListenerView + ", fiv=" + this, new Exception());
-        }
         recycle();
         mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this);
     }
diff --git a/src/com/android/launcher3/views/ListenerView.java b/src/com/android/launcher3/views/ListenerView.java
index 6e3f0ce..b2df0ee 100644
--- a/src/com/android/launcher3/views/ListenerView.java
+++ b/src/com/android/launcher3/views/ListenerView.java
@@ -17,13 +17,11 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.testing.TestProtocol;
 
 /**
  * An invisible AbstractFloatingView that can run a callback when it is being closed.
@@ -38,20 +36,12 @@
     }
 
     public void setListener(Runnable listener) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView setListener lv=" + this
-                    + ", listener=" + listener, new Exception());
-        }
         mCloseListener = listener;
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView onAttachedToWindow lv=" + this,
-                    new Exception());
-        }
         mIsOpen = true;
     }
 
@@ -59,19 +49,10 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mIsOpen = false;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView onDetachedFromView lv=" + this,
-                    new Exception());
-        }
     }
 
     @Override
     protected void handleClose(boolean animate) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView handeClose lv=" + this
-                    + ", mIsOpen=" + mIsOpen + ", mCloseListener=" + mCloseListener
-                    + ", getParent()=" + getParent(), new Exception());
-        }
         if (mIsOpen) {
             if (mCloseListener != null) {
                 mCloseListener.run();
@@ -91,10 +72,6 @@
 
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "ListenerView touchEvent lv=" + this
-                    + ", ev=" + ev, new Exception());
-        }
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             handleClose(false);
         }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index a39f215..1648bdd 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -299,7 +299,9 @@
                     launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
                             isOptionsPopupVisible(launcher)));
 
-            menu.getMenuItem(1).launch(getAppPackageName());
+            final AppIconMenuItem menuItem = menu.getMenuItem(1);
+            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
+            menuItem.launch(getAppPackageName());
         } finally {
             allApps.unfreeze();
         }
@@ -342,6 +344,7 @@
                     openMenu().
                     getMenuItem(0);
             final String shortcutName = menuItem.getText();
+            assertEquals("Wrong menu item", "Shortcut 3", shortcutName);
 
             menuItem.dragToWorkspace(false, false);
             mLauncher.getWorkspace().getWorkspaceAppIcon(shortcutName).launch(getAppPackageName());
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 153b3ce..d317783 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -144,14 +144,25 @@
     private void expectSwitchToOverviewEvents() {
     }
 
-    /**
-     * Swipes right or double presses the square button to switch to the previous app.
-     */
     @NonNull
     public Background quickSwitchToPreviousApp() {
+        boolean toRight = true;
+        quickSwitch(toRight);
+        return new Background(mLauncher);
+    }
+
+    @NonNull
+    public Background quickSwitchToPreviousAppSwipeLeft() {
+        boolean toRight = false;
+        quickSwitch(toRight);
+        return new Background(mLauncher);
+    }
+
+    @NonNull
+    private void quickSwitch(boolean toRight) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
-             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                     "want to quick switch to the previous app")) {
+            LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "want to quick switch to the previous app")) {
             verifyActiveContainer();
             final boolean launcherWasVisible = mLauncher.isLauncherVisible();
             boolean transposeInLandscape = false;
@@ -164,19 +175,36 @@
                     final int startY;
                     final int endX;
                     final int endY;
-                    if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
-                        // Swipe from the bottom left to the bottom right of the screen.
-                        startX = 0;
-                        startY = getSwipeStartY();
-                        endX = mLauncher.getDevice().getDisplayWidth();
-                        endY = startY;
+                    if (toRight) {
+                        if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
+                            // Swipe from the bottom left to the bottom right of the screen.
+                            startX = 0;
+                            startY = getSwipeStartY();
+                            endX = mLauncher.getDevice().getDisplayWidth();
+                            endY = startY;
+                        } else {
+                            // Swipe from the bottom right to the top right of the screen.
+                            startX = getSwipeStartX();
+                            startY = mLauncher.getRealDisplaySize().y - 1;
+                            endX = startX;
+                            endY = 0;
+                        }
                     } else {
-                        // Swipe from the bottom right to the top right of the screen.
-                        startX = getSwipeStartX();
-                        startY = mLauncher.getRealDisplaySize().y - 1;
-                        endX = startX;
-                        endY = 0;
+                        if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
+                            // Swipe from the bottom right to the bottom left of the screen.
+                            startX = mLauncher.getDevice().getDisplayWidth();
+                            startY = getSwipeStartY();
+                            endX = 0;
+                            endY = startY;
+                        } else {
+                            // Swipe from the bottom left to the top left of the screen.
+                            startX = getSwipeStartX();
+                            startY = 0;
+                            endX = startX;
+                            endY = mLauncher.getRealDisplaySize().y - 1;
+                        }
                     }
+
                     final boolean isZeroButton = mLauncher.getNavigationModel()
                             == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
                     LauncherInstrumentation.GestureScope gestureScope =
@@ -205,7 +233,7 @@
                     break;
             }
             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
-            return new Background(mLauncher);
+            return;
         }
     }