Add vertical layout support for Overview in portrait

WM is making changes which allows apps to maintain
their orientation independent of the orientation of
the foreground app. This allows recents to always start
in portrait even when the app currently running is in
landscape. This means we have to give the illusion of
a landscape oriented overview when user swipes up in
gesterual nav when launcher is started in portrait
configuration.

PagedOrientationHandler abstracts all coordinate specific
logic from Paged/RecentsView primarily, but also all
other dynamic calculations throughout launcher.
PagedViewOrientationState is the single point of exposure
to other classes that depend on those changes. The goal
is to also minimize holding state to allow for default
implementations of PagedOrientationHandler for all the
3p/Fallback classes. PagedViewOrientationState also
holds other data around rotation that isn't
specifically tied to view logic.

The fake landscape overview can be toggled with:
adb shell settings put global forced_rotation [0/1]

Fixes: 146176182
Change-Id: I65d8d4e9f92b93931cbe0053ccaf0cda8d2ffd6c
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 51ee216..7728207 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
@@ -39,6 +38,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.util.FloatProperty;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -50,6 +50,7 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -88,7 +89,8 @@
 
         TaskView taskView = findTaskViewToLaunch(mLauncher, v, appTargets);
 
-        AppWindowAnimationHelper helper = new AppWindowAnimationHelper(mLauncher);
+        AppWindowAnimationHelper helper =
+            new AppWindowAnimationHelper(recentsView.getPagedViewOrientedState(), mLauncher);
         anim.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, appTargets,
                 wallpaperTargets, helper).setDuration(RECENTS_LAUNCH_DURATION));
 
@@ -197,7 +199,11 @@
                 return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
             case INDEX_RECENTS_TRANSLATE_X_ANIM:
-                return new SpringAnimationBuilder<>(mLauncher.getOverviewPanel(), VIEW_TRANSLATE_X)
+                PagedOrientationHandler orientationHandler =
+                    ((RecentsView)mLauncher.getOverviewPanel()).getPagedViewOrientedState()
+                        .getOrientationHandler();
+                FloatProperty<View> translate = orientationHandler.getPrimaryViewTranslate();
+                return new SpringAnimationBuilder<>(mLauncher.getOverviewPanel(), translate)
                         .setDampingRatio(0.8f)
                         .setStiffness(250)
                         .setValues(values)
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
index 519939e..9cbe11a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -80,10 +80,10 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
         mMotionPauseDetector.clear();
 
-        super.onDragStart(start);
+        super.onDragStart(start, startDisplacement);
 
         if (handlingOverviewAnim()) {
             mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index ad4a343..19a2bae 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -126,7 +126,7 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
         initCurrentAnimation();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index 34fc3e4..ab634a4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -94,8 +94,8 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
-        super.onDragStart(start);
+    public void onDragStart(boolean start, float startDisplacement) {
+        super.onDragStart(start, startDisplacement);
 
         mReachedOverview = false;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 799f1ad..715529e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -116,12 +116,13 @@
         mLauncher = launcher;
         mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
         mShelfPeekAnim = mLauncher.getShelfPeekAnim();
+        mRecentsView = mLauncher.getOverviewPanel();
         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
-        mYRange = LayoutUtils.getShelfTrackingDistance(mLauncher, mLauncher.getDeviceProfile());
+        mYRange = LayoutUtils.getShelfTrackingDistance(
+            mLauncher, mLauncher.getDeviceProfile());
         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
-        mRecentsView = mLauncher.getOverviewPanel();
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 912be98..d5b221d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -92,8 +92,8 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
-        super.onDragStart(start);
+    public void onDragStart(boolean start, float startDisplacement) {
+        super.onDragStart(start, startDisplacement);
         mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
         mTaskToLaunch = mLauncher.<RecentsView>getOverviewPanel().getTaskViewAt(0);
         ActivityManagerWrapper.getInstance()
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index f79ad25..e0532ac 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.FlingBlockCheck;
@@ -77,7 +78,9 @@
     public TaskViewTouchController(T activity) {
         mActivity = activity;
         mRecentsView = activity.getOverviewPanel();
-        mDetector = new SingleAxisSwipeDetector(activity, this, SingleAxisSwipeDetector.VERTICAL);
+        SingleAxisSwipeDetector.Direction dir =
+            mRecentsView.getPagedOrientationHandler().getOppositeSwipeDirection();
+        mDetector = new SingleAxisSwipeDetector(activity, this, dir);
     }
 
     private boolean canInterceptTouch() {
@@ -190,15 +193,18 @@
             mPendingAnimation = null;
         }
 
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         mCurrentAnimationIsGoingUp = goingUp;
         BaseDragLayer dl = mActivity.getDragLayer();
-        long maxDuration = (long) (2 * dl.getHeight());
-
+        final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
+        long maxDuration = (long) (2 * secondaryLayerDimension);
+        int verticalFactor = -orientationHandler.getTaskDismissDirectionFactor();
+        int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
         if (goingUp) {
             mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
                     true /* animateTaskView */, true /* removeTask */, maxDuration);
 
-            mEndDisplacement = -mTaskBeingDragged.getHeight();
+            mEndDisplacement = -secondaryTaskDimension;
         } else {
             mPendingAnimation = mRecentsView.createTaskLaunchAnimation(
                     mTaskBeingDragged, maxDuration, Interpolators.ZOOM_IN);
@@ -207,6 +213,7 @@
             dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
             mEndDisplacement = dl.getHeight() - mTempCords[1];
         }
+        mEndDisplacement *= verticalFactor;
 
         if (mCurrentAnimation != null) {
             mCurrentAnimation.setOnCancelRunnable(null);
@@ -220,9 +227,10 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         if (mCurrentAnimation == null) {
-            reInitAnimationController(mDetector.wasInitialTouchPositive());
+            reInitAnimationController(orientationHandler.isGoingUp(startDisplacement));
             mDisplacementShift = 0;
         } else {
             mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
@@ -233,9 +241,10 @@
 
     @Override
     public boolean onDrag(float displacement) {
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         float totalDisplacement = displacement + mDisplacementShift;
-        boolean isGoingUp =
-                totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0;
+        boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
+                orientationHandler.isGoingUp(totalDisplacement);
         if (isGoingUp != mCurrentAnimationIsGoingUp) {
             reInitAnimationController(isGoingUp);
             mFlingBlockCheck.blockFling();
@@ -262,11 +271,12 @@
         if (blockedFling) {
             fling = false;
         }
+        PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
         float progress = mCurrentAnimation.getProgressFraction();
         float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
         if (fling) {
             logAction = Touch.FLING;
-            boolean goingUp = velocity < 0;
+            boolean goingUp = orientationHandler.isGoingUp(velocity);
             goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
         } else {
             logAction = Touch.SWIPE;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index 59b117f..375f160 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -125,7 +125,8 @@
             return anim;
         }
 
-        final AppWindowAnimationHelper clipHelper = new AppWindowAnimationHelper(mActivity);
+        final AppWindowAnimationHelper clipHelper = new AppWindowAnimationHelper(
+            mRecentsView.getPagedViewOrientedState(), mActivity);
 
         // At this point, the activity is already started and laid-out. Get the home-bounds
         // relative to the screen using the rootView of the activity.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 3601af2..8957b0d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -45,7 +45,9 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.model.PagedViewOrientedState;
+import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
@@ -94,7 +96,7 @@
     protected final BaseActivityInterface<T> mActivityInterface;
     protected final InputConsumerController mInputConsumer;
 
-    protected final AppWindowAnimationHelper mAppWindowAnimationHelper;
+    protected AppWindowAnimationHelper mAppWindowAnimationHelper;
     protected final TransformParams mTransformParams = new TransformParams();
 
     // Shift in the range of [0, 1].
@@ -123,6 +125,8 @@
     protected boolean mCanceled;
     protected int mFinishingRecentsAnimationForNewTaskId = -1;
 
+    protected PagedViewOrientedState mOrientedState;
+
     protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             GestureState gestureState, InputConsumerController inputConsumer) {
         mContext = context;
@@ -132,20 +136,18 @@
         mActivityInitListener =
                 mActivityInterface.createActivityInitListener(this::onActivityInit);
         mInputConsumer = inputConsumer;
-
         mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
         mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
-
         initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
-                .getDeviceProfile(mContext));
+            .getDeviceProfile(mContext));
     }
 
     protected void performHapticFeedback() {
         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
     }
 
-    public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
-        return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null;
+    public Consumer<MotionEvent> getRecentsViewDispatcher() {
+        return mRecentsView != null ? mRecentsView.getEventDispatcher() : null;
     }
 
     @UiThread
@@ -326,10 +328,19 @@
             // we otherwise use the minimized home bounds provided by the system.
             mAppWindowAnimationHelper.updateHomeBounds(getStackBounds(dp));
         }
+        int displayRotation = 0;
+        if (mOrientedState != null) {
+            // TODO(b/150300347): The first recents animation after launcher is started with the
+            //  foreground app not in landscape will look funky until that bug is fixed
+            displayRotation = mOrientedState.getDisplayRotation();
+        }
+        RotationHelper.getTargetRectForRotation(TEMP_RECT, dp.widthPx, dp.heightPx,
+            displayRotation);
         mAppWindowAnimationHelper.updateTargetRect(TEMP_RECT);
         if (mDeviceState.isFullyGesturalNavMode()) {
             // We can drag all the way to the top of the screen.
-            mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
+            mDragLengthFactor = orientationHandler
+                .getDragLengthFactor(dp.heightPx, mTransitionDragLength);
         }
     }
 
@@ -338,7 +349,17 @@
      */
     protected abstract boolean moveWindowWithRecentsScroll();
 
-    protected abstract boolean onActivityInit(Boolean alreadyOnHome);
+    protected boolean onActivityInit(Boolean alreadyOnHome) {
+        T createdActivity = mActivityInterface.getCreatedActivity();
+        if (createdActivity != null) {
+            mOrientedState = ((RecentsView) createdActivity.getOverviewPanel())
+                .getPagedViewOrientedState();
+            mAppWindowAnimationHelper = new AppWindowAnimationHelper(mOrientedState, mContext);
+            initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
+                .getDeviceProfile(mContext));
+        }
+        return true;
+    }
 
     /**
      * Called to create a input proxy for the running task
@@ -381,21 +402,24 @@
      * Applies the transform on the recents animation without any additional null checks
      */
     protected void applyTransformUnchecked() {
+        PagedOrientationHandler handler = mOrientedState.getOrientationHandler();
         float shift = mCurrentShift.value;
-        float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
-        float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
-        mAppWindowAnimationHelper.getTargetRect().width());
+        float offset = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
+        float taskSize = handler.getPrimarySize(mAppWindowAnimationHelper.getTargetRect());
+        float offsetScale = getTaskCurveScaleForOffset(offset, taskSize);
         mTransformParams.setProgress(shift)
-                .setOffsetX(offsetX)
+                .setOffset(offset)
                 .setOffsetScale(offsetScale)
                 .setTargetSet(mRecentsAnimationTargets)
-                .setLauncherOnTop(true);
+                .setLauncherOnTop(true)
+                .setPagedOrientedState(mOrientedState);
         mAppWindowAnimationHelper.applyTransform(mTransformParams);
     }
 
-    private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
-        float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + mPageSpacing;
-        float interpolation = Math.min(1, offsetX / distanceToReachEdge);
+    private float getTaskCurveScaleForOffset(float offset, float taskSize) {
+        int dpPixel = getOrientationHandler().getShortEdgeLength(mDp);
+        float distanceToReachEdge = dpPixel / 2 + taskSize / 2 + mPageSpacing;
+        float interpolation = Math.min(1, offset / distanceToReachEdge);
         return TaskView.getCurveScaleForInterpolation(interpolation);
     }
 
@@ -410,9 +434,9 @@
                 mAppWindowAnimationHelper.applyTransform(
                         mTransformParams.setProgress(startProgress)
                                 .setTargetSet(mRecentsAnimationTargets)
+                                .setPagedOrientedState(mOrientedState)
                                 .setLauncherOnTop(false)));
         final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
-
         final View floatingView = homeAnimationFactory.getFloatingView();
         final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
         RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext.getResources());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index 6574d22..71580ca 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -30,10 +30,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.touch.PortraitPagedViewHandler;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.util.ActivityInitListener;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index cd001a1..0ae1a3a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -161,12 +161,12 @@
 
     @Override
     protected boolean onActivityInit(Boolean alreadyOnHome) {
+        super.onActivityInit(alreadyOnHome);
         mActivity = mActivityInterface.getCreatedActivity();
         mRecentsView = mActivity.getOverviewPanel();
         linkRecentsViewScroll();
         mRecentsView.setDisallowScrollToClearAll(true);
         mRecentsView.getClearAllButton().setVisibilityAlpha(0);
-
         mRecentsView.setZoomProgress(1);
 
         if (!mContinuingLastGesture) {
@@ -177,6 +177,7 @@
             }
         }
         mStateCallback.setStateOnUiThread(STATE_RECENTS_PRESENT);
+        mDeviceState.enableMultipleRegions(false);
         return true;
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 1b6d291..999f2d7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -54,6 +54,8 @@
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.touch.PortraitPagedViewHandler;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -268,23 +270,28 @@
                     float scrollOffsetX = recentsView.getScrollOffset();
                     float offscreenX = recentsView.getOffscreenTranslationX(currScale);
 
-                    float fromTranslationX = attached ? offscreenX - scrollOffsetX : 0;
-                    float toTranslationX = attached ? 0 : offscreenX - scrollOffsetX;
+                    float fromTranslation = attached ? offscreenX - scrollOffsetX : 0;
+                    float toTranslation = attached ? 0 : offscreenX - scrollOffsetX;
                     launcher.getStateManager()
                             .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
 
+                    PagedOrientationHandler pagedOrientationHandler =
+                        recentsView.getPagedViewOrientedState().getOrientationHandler();
                     if (!recentsView.isShown() && animate) {
-                        recentsView.setTranslationX(fromTranslationX);
+                        pagedOrientationHandler
+                            .getPrimaryViewTranslate().set(recentsView, fromTranslation);
                     } else {
-                        fromTranslationX = recentsView.getTranslationX();
+                        fromTranslation =
+                            pagedOrientationHandler.getPrimaryViewTranslate().get(recentsView);
                     }
 
                     if (!animate) {
-                        recentsView.setTranslationX(toTranslationX);
+                        pagedOrientationHandler
+                            .getPrimaryViewTranslate().set(recentsView, toTranslation);
                     } else {
                         launcher.getStateManager().createStateElementAnimation(
                                 INDEX_RECENTS_TRANSLATE_X_ANIM,
-                                fromTranslationX, toTranslationX).start();
+                                fromTranslation, toTranslation).start();
                     }
 
                     fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index e0b8a37..c4466e7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -64,6 +64,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.model.PagedViewOrientedState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -261,6 +262,7 @@
 
     @Override
     protected boolean onActivityInit(Boolean alreadyOnHome) {
+        super.onActivityInit(alreadyOnHome);
         final T activity = mActivityInterface.getCreatedActivity();
         if (mActivity == activity) {
             return true;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 3f5179f..94b0051 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -183,7 +183,8 @@
             RemoteAnimationTargetCompat[] wallpaperTargets) {
         AnimatorSet target = new AnimatorSet();
         boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
-        AppWindowAnimationHelper helper = new AppWindowAnimationHelper(this);
+        AppWindowAnimationHelper helper = new AppWindowAnimationHelper(
+            mFallbackRecentsView.getPagedViewOrientedState(), this);
         target.play(getRecentsWindowAnimator(taskView, !activityClosing, appTargets,
                 wallpaperTargets, helper).setDuration(RECENTS_LAUNCH_DURATION));
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
index 8d73591..aedb756 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
@@ -130,6 +130,7 @@
                     .setLauncherOnTop(true);
 
         final RecentsView recentsView = v.getRecentsView();
+        params.setPagedOrientedState(recentsView.getPagedViewOrientedState());
         final ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
         appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
         appAnimator.addUpdateListener(new MultiValueUpdateListener() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index e1b5df0..7617ffe 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -56,6 +56,8 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.config.FeatureFlags;
@@ -80,6 +82,7 @@
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistantUtilities;
 import com.android.quickstep.util.ProtoTracer;
+import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.IOverviewProxy;
@@ -537,6 +540,22 @@
         return base;
     }
 
+    private void handleOrientationSetup(InputConsumer baseInputConsumer) {
+        if (!PagedView.sFlagForcedRotation) {
+            return;
+        }
+        mDeviceState.enableMultipleRegions(baseInputConsumer instanceof OtherActivityInputConsumer);
+        Launcher l = (Launcher) mOverviewComponentObserver
+            .getActivityInterface().getCreatedActivity();
+        if (l == null || !(l.getOverviewPanel() instanceof RecentsView)) {
+            return;
+        }
+        ((RecentsView)l.getOverviewPanel())
+            .setLayoutRotation(mDeviceState.getCurrentActiveRotation(),
+                mDeviceState.getDisplayRotation());
+        l.getDragLayer().recreateControllers();
+    }
+
     private InputConsumer newBaseConsumer(GestureState previousGestureState,
             GestureState gestureState, MotionEvent event) {
         if (mDeviceState.isKeyguardShowingOccluded()) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 79e71a1..dc0c194 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -195,4 +195,9 @@
         }
         super.applyLoadPlan(tasks);
     }
+
+    @Override
+    protected boolean supportsVerticalLandscape() {
+        return false;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 2f8682f..bd9f330 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -176,8 +176,7 @@
         // Proxy events to recents view
         if (mPassedWindowMoveSlop && mInteractionHandler != null
                 && !mRecentsViewDispatcher.hasConsumer()) {
-            mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher(
-                    mNavBarPosition.getRotationMode()));
+            mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher());
         }
         int edgeFlags = ev.getEdgeFlags();
         ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
index 5a9c2fe..6923ca2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
@@ -15,12 +15,6 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
-import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.Matrix;
@@ -37,6 +31,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.model.PagedViewOrientedState;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.SystemUiProxy;
@@ -50,6 +45,12 @@
 import com.android.systemui.shared.system.TransactionCompat;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
+import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+
 /**
  * Utility class to handle window clip animation
  */
@@ -82,6 +83,7 @@
     private final Rect mTmpRect = new Rect();
     private final RectF mTmpRectF = new RectF();
     private final RectF mCurrentRectWithInsets = new RectF();
+    private PagedViewOrientedState mOrientedState;
     // Corner radius of windows, in pixels
     private final float mWindowCornerRadius;
     // Corner radius of windows when they're in overview mode.
@@ -100,20 +102,24 @@
     private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
     private TargetAlphaProvider mBaseAlphaCallback = (t, a) -> 1;
 
-    public AppWindowAnimationHelper(Context context) {
+    public AppWindowAnimationHelper(PagedViewOrientedState orientedState, Context context) {
+        mOrientedState = orientedState;
         mWindowCornerRadius = getWindowCornerRadius(context.getResources());
         mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
         mTaskCornerRadius = TaskCornerRadius.get(context);
         mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows;
     }
 
+    public AppWindowAnimationHelper(Context context) {
+        this(null, context);
+    }
+
     private void updateSourceStack(RemoteAnimationTargetCompat target) {
         mSourceInsets.set(target.contentInsets);
         mSourceStackBounds.set(target.sourceContainerBounds);
 
         // TODO: Should sourceContainerBounds already have this offset?
         mSourceStackBounds.offsetTo(target.position.x, target.position.y);
-
     }
 
     public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
@@ -138,8 +144,9 @@
         // from the source rect. The difference between the target rect (scaled to the
         // source rect) is the amount to clip on each edge.
         RectF scaledTargetRect = new RectF(mTargetRect);
-        Utilities.scaleRectFAboutCenter(scaledTargetRect,
-                mSourceRect.width() / mTargetRect.width());
+        float scale = getSrcToTargetScale();
+        Utilities.scaleRectFAboutCenter(scaledTargetRect, scale);
+
         scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
         mSourceWindowClipInsets.set(
                 Math.max(scaledTargetRect.left, 0),
@@ -149,6 +156,15 @@
         mSourceRect.set(scaledTargetRect);
     }
 
+    private float getSrcToTargetScale() {
+        if (mOrientedState == null) {
+            return mSourceRect.width() / mTargetRect.width();
+        } else {
+            return mOrientedState.getOrientationHandler()
+                .getCurrentAppAnimationScale(mSourceRect, mTargetRect);
+        }
+    }
+
     public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
         mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
         mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode;
@@ -221,7 +237,6 @@
                     layer = Integer.MAX_VALUE;
                 }
             }
-
             // Since radius is in Surface space, but we draw the rounded corners in screen space, we
             // have to undo the scale.
             surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer,
@@ -237,11 +252,16 @@
             mTmpRectF.set(mTargetRect);
             Utilities.scaleRectFAboutCenter(mTmpRectF, params.mOffsetScale);
             mCurrentRect.set(mRectFEvaluator.evaluate(params.mProgress, mSourceRect, mTmpRectF));
-            mCurrentRect.offset(params.mOffsetX, 0);
+            if (mOrientedState == null || mOrientedState.areMultipleLayoutOrientationsDisabled()) {
+                mCurrentRect.offset(params.mOffset, 0);
+            } else {
+                int displayRotation = mOrientedState.getDisplayRotation();
+                mOrientedState.getOrientationHandler().offsetTaskRect(mCurrentRect,
+                    params.mOffset, displayRotation);
+            }
         }
 
         updateClipRect(params);
-
         return mCurrentRect;
     }
 
@@ -340,7 +360,7 @@
      * @return The source rect's scale and translation relative to the target rect.
      */
     public LauncherState.ScaleAndTranslation getScaleAndTranslation() {
-        float scale = mSourceRect.width() / mTargetRect.width();
+        float scale = getSrcToTargetScale();
         float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
         return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
     }
@@ -390,7 +410,7 @@
 
     public static class TransformParams {
         private float mProgress;
-        private float mOffsetX;
+        private float mOffset;
         private float mOffsetScale;
         private @Nullable RectF mCurrentRect;
         private float mTargetAlpha;
@@ -401,7 +421,7 @@
 
         public TransformParams() {
             mProgress = 0;
-            mOffsetX = 0;
+            mOffset = 0;
             mOffsetScale = 1;
             mCurrentRect = null;
             mTargetAlpha = 1;
@@ -453,8 +473,8 @@
          * the default), then offset the current rect by this amount after computing the rect based
          * on {@link #mProgress}.
          */
-        public TransformParams setOffsetX(float offsetX) {
-            mOffsetX = offsetX;
+        public TransformParams setOffset(float offset) {
+            mOffset = offset;
             return this;
         }
 
@@ -504,8 +524,8 @@
             return mProgress;
         }
 
-        public float getOffsetX() {
-            return mOffsetX;
+        public float getOffset() {
+            return mOffset;
         }
 
         public float getOffsetScale() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
index 9db0c09..d0819c1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
@@ -21,7 +21,7 @@
 import android.util.Property;
 import android.widget.Button;
 
-import com.android.launcher3.Utilities;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
 
@@ -44,21 +44,26 @@
     private float mContentAlpha = 1;
     private float mVisibilityAlpha = 1;
 
-    private final boolean mIsRtl;
+    private boolean mIsRtl;
 
     private int mScrollOffset;
+    private RecentsView mParent;
 
     public ClearAllButton(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mIsRtl = Utilities.isRtl(context.getResources());
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
+        mScrollOffset = mIsRtl ? mParent.getPaddingRight() / 2 : - mParent.getPaddingLeft() / 2;
+    }
 
-        RecentsView parent = (RecentsView) getParent();
-        mScrollOffset = mIsRtl ? parent.getPaddingRight() / 2 : - parent.getPaddingLeft() / 2;
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mParent = (RecentsView) getParent();
+        mIsRtl = !mParent.getPagedOrientationHandler().getRecentsRtlSetting(getResources());
     }
 
     @Override
@@ -73,6 +78,21 @@
         }
     }
 
+    public void onLayoutChanged() {
+        if (mParent == null) {
+            return;
+        }
+        setRotation(mParent.getPagedOrientationHandler().getDegreesRotated());
+    }
+
+    public void setRtl(boolean rtl) {
+        if (mIsRtl == rtl) {
+            return;
+        }
+        mIsRtl = rtl;
+        invalidate();
+    }
+
     public void setVisibilityAlpha(float alpha) {
         if (mVisibilityAlpha != alpha) {
             mVisibilityAlpha = alpha;
@@ -82,14 +102,16 @@
 
     @Override
     public void onPageScroll(ScrollState scrollState) {
-        float width = getWidth();
-        if (width == 0) {
+        PagedOrientationHandler orientationHandler = mParent.getPagedOrientationHandler();
+        float orientationSize = orientationHandler.getPrimaryValue(getWidth(), getHeight());
+        if (orientationSize == 0) {
             return;
         }
 
-        float shift = Math.min(scrollState.scrollFromEdge, width);
-        setTranslationX(mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift));
-        mScrollAlpha = 1 - shift / width;
+        float shift = Math.min(scrollState.scrollFromEdge, orientationSize);
+        float translation = mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift);
+        orientationHandler.setPrimaryAndResetSecondaryTranslate(this, translation);
+        mScrollAlpha = 1 - shift / orientationSize;
         updateAlpha();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 3e106aa..b2d182b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -42,6 +42,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.StateListener;
+import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
@@ -180,19 +181,21 @@
      * @return The translationX to apply to this view so that the first task is just offscreen.
      */
     public float getOffscreenTranslationX(float recentsScale) {
-        float offscreenX = NORMAL.getOverviewScaleAndTranslation(mActivity).translationX;
+        LauncherState.ScaleAndTranslation overviewScaleAndTranslation =
+            NORMAL.getOverviewScaleAndTranslation(mActivity);
+        float offscreen = mOrientationHandler.getTranslationValue(overviewScaleAndTranslation);
         // Offset since scale pushes tasks outwards.
         getTaskSize(sTempRect);
-        int taskWidth = sTempRect.width();
-        offscreenX += taskWidth * (recentsScale - 1) / 2;
+        int taskSize = mOrientationHandler.getPrimarySize(sTempRect);
+        offscreen += taskSize * (recentsScale - 1) / 2;
         if (mRunningTaskTileHidden) {
             // The first task is hidden, so offset by its width.
-            offscreenX -= (taskWidth + getPageSpacing()) * recentsScale;
+            offscreen -= (taskSize + getPageSpacing()) * recentsScale;
         }
         if (isRtl()) {
-            offscreenX = -offscreenX;
+            offscreen = -offscreen;
         }
-        return offscreenX;
+        return offscreen;
     }
 
     @Override
@@ -277,6 +280,11 @@
     }
 
     @Override
+    protected boolean supportsVerticalLandscape() {
+        return PagedView.sFlagForcedRotation;
+    }
+
+    @Override
     public void reset() {
         super.reset();
 
@@ -339,19 +347,19 @@
     }
 
     @Override
-    protected int computeMinScrollX() {
+    protected int computeMinScroll() {
         if (canComputeScrollX() && !mIsRtl) {
             return computeScrollX();
         }
-        return super.computeMinScrollX();
+        return super.computeMinScroll();
     }
 
     @Override
-    protected int computeMaxScrollX() {
+    protected int computeMaxScroll() {
         if (canComputeScrollX() && mIsRtl) {
             return computeScrollX();
         }
-        return super.computeMaxScrollX();
+        return super.computeMaxScroll();
     }
 
     private boolean canComputeScrollX() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 27ef93c..50a6629 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
@@ -52,7 +51,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Canvas;
-import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -66,11 +64,13 @@
 import android.text.TextPaint;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Property;
 import android.util.SparseBooleanArray;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
+import android.view.OrientationEventListener;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
@@ -90,12 +90,15 @@
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.anim.SpringObjectAnimator;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -162,6 +165,8 @@
                 }
             };
 
+    private final OrientationEventListener mOrientationListener;
+    private int mPreviousRotation;
     protected RecentsAnimationController mRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected AppWindowAnimationHelper mAppWindowAnimationHelper;
@@ -340,7 +345,8 @@
         mActivity = (T) BaseActivity.fromContext(context);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
-        mTempAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
+        mTempAppWindowAnimationHelper =
+            new AppWindowAnimationHelper(getPagedViewOrientedState(), context);
 
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
@@ -348,7 +354,7 @@
         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
                 10 /* initial size */);
 
-        mIsRtl = !Utilities.isRtl(getResources());
+        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
@@ -368,9 +374,21 @@
                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
         setWillNotDraw(false);
         updateEmptyMessage();
+        disableMultipleLayoutRotations(!supportsVerticalLandscape());
 
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
+        mOrientationListener = new OrientationEventListener(getContext()) {
+            @Override
+            public void onOrientationChanged(int i) {
+                int rotation = RotationHelper.getRotationFromDegrees(i, mPreviousRotation);
+                if (mPreviousRotation != rotation) {
+                    animateRecentsRotationInPlace(rotation);
+                    mPreviousRotation = rotation;
+                }
+            }
+        };
+
     }
 
     public OverScroller getScroller() {
@@ -496,6 +514,13 @@
     }
 
     public void setOverviewStateEnabled(boolean enabled) {
+        if (supportsVerticalLandscape() && mOrientationListener.canDetectOrientation()) {
+            if (enabled) {
+                mOrientationListener.enable();
+            } else {
+                mOrientationListener.disable();
+            }
+        }
         mOverviewStateEnabled = enabled;
         updateTaskStackListenerState();
         if (!enabled) {
@@ -623,7 +648,7 @@
             final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
-            taskView.bind(task);
+            taskView.bind(task, mLayoutRotation);
         }
 
         if (mNextPage == INVALID_PAGE) {
@@ -755,19 +780,21 @@
         if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
             return;
         }
-        int scrollX = getScrollX();
-        final int halfPageWidth = getNormalChildWidth() / 2;
-        final int screenCenter = mInsets.left + getPaddingLeft() + scrollX + halfPageWidth;
-        final int halfScreenWidth = getMeasuredWidth() / 2;
+        CurveProperties curveProperties = mOrientationHandler
+            .getCurveProperties(this, mInsets);
+        int scroll = curveProperties.scroll;
+        final int halfPageSize = curveProperties.halfPageSize;
+        final int screenCenter = curveProperties.screenCenter;
+        final int halfScreenSize = curveProperties.halfScreenSize;
         final int pageSpacing = mPageSpacing;
-        mScrollState.scrollFromEdge = mIsRtl ? scrollX : (mMaxScrollX - scrollX);
+        mScrollState.scrollFromEdge = mIsRtl ? scroll : (mMaxScroll - scroll);
 
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth;
+            float pageCenter = mOrientationHandler.getViewCenterPosition(page) + halfPageSize;
             float distanceFromScreenCenter = screenCenter - pageCenter;
-            float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
+            float distanceToReachEdge = halfScreenSize + halfPageSize + pageSpacing;
             mScrollState.linearInterpolation = Math.min(1,
                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
             ((PageCallbacks) page).onPageScroll(mScrollState);
@@ -915,6 +942,47 @@
         setSwipeDownShouldLaunchApp(true);
     }
 
+    private void animateRecentsRotationInPlace(int newRotation) {
+        if (!supportsVerticalLandscape()) {
+            return;
+        }
+
+        AnimatorSet pa = setRecentsChangedOrientation(true);
+        pa.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                updateLayoutRotation(newRotation);
+                ((DragLayer)mActivity.getDragLayer()).recreateControllers();
+                rotateAllChildTasks();
+                setRecentsChangedOrientation(false).start();
+            }
+        });
+        pa.start();
+    }
+
+    public AnimatorSet setRecentsChangedOrientation(boolean fadeInChildren) {
+        getRunningTaskIndex();
+        int runningIndex = getCurrentPage();
+        AnimatorSet as = new AnimatorSet();
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            if (runningIndex == i) {
+                continue;
+            }
+            View taskView = getTaskViewAt(i);
+            as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1));
+        }
+        return as;
+    }
+
+    abstract protected boolean supportsVerticalLandscape();
+
+    private void rotateAllChildTasks() {
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            TaskView taskView = getTaskViewAt(i);
+            taskView.setOverviewRotation(mLayoutRotation);
+        }
+    }
+
     /**
      * Called when a gesture from an app has finished.
      */
@@ -950,7 +1018,7 @@
                     new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0,
                     false, true, false, false, new ActivityManager.TaskDescription(), 0,
                     new ComponentName("", ""), false);
-            taskView.bind(mTmpRunningTask);
+            taskView.bind(mTmpRunningTask, mLayoutRotation);
         }
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
@@ -1126,15 +1194,18 @@
 
     private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
         addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
+        FloatProperty<View> secondaryViewTranslate =
+            mOrientationHandler.getSecondaryViewTranslate();
+        int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
+        int verticalFactor = mOrientationHandler.getTaskDismissDirectionFactor();
         if (UNSTABLE_SPRINGS.get() && taskView instanceof TaskView) {
-            addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y,
-                            MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
-                            SpringForce.STIFFNESS_MEDIUM,
-                            0, -taskView.getHeight()),
-                    duration, LINEAR, anim);
+            addAnim(new SpringObjectAnimator<>(taskView, secondaryViewTranslate,
+                    MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
+                    SpringForce.STIFFNESS_MEDIUM, 0, verticalFactor * secondaryTaskDimension),
+                duration, LINEAR, anim);
         } else {
-            addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
-                    duration, LINEAR, anim);
+            addAnim(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
+                verticalFactor * secondaryTaskDimension), duration, LINEAR, anim);
         }
     }
 
@@ -1165,11 +1236,9 @@
         }
 
         int[] oldScroll = new int[count];
-        getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
-
         int[] newScroll = new int[count];
+        getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
         getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
-
         int taskCount = getTaskViewCount();
         int scrollDiffPerPage = 0;
         if (count > 1) {
@@ -1212,8 +1281,9 @@
                                 SpringForce.STIFFNESS_MEDIUM,
                                 0, scrollDiff), duration, ACCEL, anim);
                     } else {
-                        addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), duration,
-                                ACCEL, anim);
+                        Property translationProperty = mOrientationHandler.getPrimaryViewTranslate();
+                        addAnim(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff),
+                            duration, ACCEL, anim);
                     }
 
                     needsCurveUpdates = true;
@@ -1430,6 +1500,18 @@
     }
 
     @Override
+    public void setLayoutRotation(int touchRotation, int displayRotation) {
+        if (!sFlagForcedRotation) {
+            return;
+        }
+
+        super.setLayoutRotation(touchRotation, displayRotation);
+        mClearAllButton.onLayoutChanged();
+        mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
+        setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
+    }
+
+    @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
         child.setAlpha(mContentAlpha);
@@ -1649,7 +1731,8 @@
             }
         });
 
-        AppWindowAnimationHelper appWindowAnimationHelper = new AppWindowAnimationHelper(mActivity);
+        AppWindowAnimationHelper appWindowAnimationHelper = new AppWindowAnimationHelper(
+            getPagedViewOrientedState(), mActivity);
         appWindowAnimationHelper.fromTaskThumbnailView(tv.getThumbnail(), this);
         appWindowAnimationHelper.prepareAnimation(mActivity.getDeviceProfile(), true /* isOpening */);
         AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, appWindowAnimationHelper);
@@ -1806,7 +1889,7 @@
     }
 
     @Override
-    protected int computeMinScrollX() {
+    protected int computeMinScroll() {
         if (getTaskViewCount() > 0) {
             if (mDisallowScrollToClearAll) {
                 // We aren't showing the clear all button,
@@ -1821,11 +1904,11 @@
             }
             return getScrollForPage(mTaskViewStartIndex);
         }
-        return super.computeMinScrollX();
+        return super.computeMinScroll();
     }
 
     @Override
-    protected int computeMaxScrollX() {
+    protected int computeMaxScroll() {
         if (getTaskViewCount() > 0) {
             if (mDisallowScrollToClearAll) {
                 // We aren't showing the clear all button,
@@ -1840,7 +1923,7 @@
             }
             return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1);
         }
-        return super.computeMaxScrollX();
+        return super.computeMaxScroll();
     }
 
     public ClearAllButton getClearAllButton() {
@@ -1855,31 +1938,25 @@
             return 0;
         }
         int startScroll = getScrollForPage(getRunningTaskIndex());
-        int offsetX = startScroll - getScrollX();
-        offsetX *= getScaleX();
+        int offsetX = startScroll - mOrientationHandler.getPrimaryScroll(this);
+        offsetX *= mOrientationHandler.getPrimaryScale(this);
         return offsetX;
     }
 
-    public Consumer<MotionEvent> getEventDispatcher(RotationMode rotationMode) {
-        if (rotationMode.isTransposed) {
-            Matrix transform = new Matrix();
-            transform.setRotate(-rotationMode.surfaceRotation);
-
-            if (getWidth() > 0 && getHeight() > 0) {
-                float scale = ((float) getWidth()) / getHeight();
-                transform.postScale(scale, 1 / scale);
-            }
-
-            Matrix inverse = new Matrix();
-            transform.invert(inverse);
-            return e -> {
-                e.transform(transform);
-                super.onTouchEvent(e);
-                e.transform(inverse);
-            };
-        } else {
+    public Consumer<MotionEvent> getEventDispatcher() {
+        int degreesRotated = RotationHelper.getDegreesFromRotation(mLayoutRotation);
+        if (degreesRotated == 0) {
             return super::onTouchEvent;
         }
+
+        // At this point the event coordinates have already been transformed, so we need to
+        // undo that transformation since PagedView also accommodates for the transformation via
+        // PagedOrientationHandler
+        return e -> {
+            RotationHelper.transformEvent(-degreesRotated, e, true);
+            super.onTouchEvent(e);
+            RotationHelper.transformEvent(-degreesRotated, e, false);
+        };
     }
 
     public AppWindowAnimationHelper getClipAnimationHelper() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 8ed1392..178ff32 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -36,6 +36,7 @@
 import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.util.Property;
 import android.view.Surface;
 import android.view.View;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 79b9a9d..9150cc7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -16,15 +16,6 @@
 
 package com.android.quickstep.views;
 
-import static android.widget.Toast.LENGTH_SHORT;
-
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
@@ -41,7 +32,7 @@
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
-import android.view.Gravity;
+import android.view.Surface;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -58,8 +49,10 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.states.RotationHelper;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -83,6 +76,20 @@
 import java.util.List;
 import java.util.function.Consumer;
 
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.Gravity.END;
+import static android.view.Gravity.START;
+import static android.view.Gravity.TOP;
+import static android.widget.Toast.LENGTH_SHORT;
+import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+
 /**
  * A task in the Recents view.
  */
@@ -186,6 +193,8 @@
     private float mFooterVerticalOffset = 0;
     private float mFooterAlpha = 1;
     private int mStackHeight;
+    private boolean mHideActionsView;
+    private PagedOrientationHandler mOrientationHandler;
 
     public TaskView(Context context) {
         this(context, null);
@@ -244,7 +253,7 @@
             if (mActionsView != null) {
                 TaskView.LayoutParams params = new TaskView.LayoutParams(LayoutParams.MATCH_PARENT,
                         getResources().getDimensionPixelSize(R.dimen.overview_actions_height),
-                        Gravity.BOTTOM);
+                        BOTTOM);
                 addView(mActionsView, params);
                 mActionsView.setAlpha(0);
             }
@@ -266,10 +275,11 @@
     /**
      * Updates this task view to the given {@param task}.
      */
-    public void bind(Task task) {
+    public void bind(Task task, int recentsRotation) {
         cancelPendingLoadTasks();
         mTask = task;
         mSnapshotView.bind(task);
+        setOverviewRotation(recentsRotation);
     }
 
     public Task getTask() {
@@ -439,6 +449,45 @@
         }
     }
 
+    void setOverviewRotation(int iconRotation) {
+        PagedOrientationHandler orientationHandler = getRecentsView().getPagedOrientationHandler();
+        boolean isRtl = orientationHandler.getRecentsRtlSetting(getResources());
+        LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
+        snapshotParams.bottomMargin = LayoutUtils.thumbnailBottomMargin(getContext());
+        int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+        LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
+        int rotation = RotationHelper.getDegreesFromRotation(iconRotation);
+        mHideActionsView = true;
+        switch (iconRotation) {
+            case Surface.ROTATION_90:
+                iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
+                iconParams.rightMargin = -thumbnailPadding;
+                iconParams.leftMargin = iconParams.topMargin = iconParams.bottomMargin = 0;
+                break;
+            case Surface.ROTATION_180:
+                iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
+                iconParams.bottomMargin = -thumbnailPadding;
+                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
+                break;
+            case Surface.ROTATION_270:
+                iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
+                iconParams.leftMargin = -thumbnailPadding;
+                iconParams.rightMargin = iconParams.topMargin = iconParams.bottomMargin = 0;
+                break;
+            case Surface.ROTATION_0:
+            default:
+                iconParams.gravity = TOP | CENTER_HORIZONTAL;
+                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin =
+                    iconParams.bottomMargin = 0;
+                mHideActionsView = false;
+                break;
+        }
+        mSnapshotView.setLayoutParams(snapshotParams);
+        mIconView.setLayoutParams(iconParams);
+        mIconView.setRotation(rotation);
+        updateActionsViewVisibility(!mHideActionsView);
+    }
+
     private void setIconAndDimTransitionProgress(float progress, boolean invert) {
         if (invert) {
             progress = 1 - progress;
@@ -601,8 +650,7 @@
 
             addView(view, indexToAdd);
             LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
-            layoutParams.gravity =
-                    Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+            layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
             layoutParams.bottomMargin =
                     ((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin;
             view.setAlpha(mFooterAlpha);
@@ -855,9 +903,7 @@
         mFullscreenProgress = progress;
         boolean isFullscreen = mFullscreenProgress > 0;
         mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
-        if (mActionsView != null) {
-            mActionsView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
-        }
+        updateActionsViewVisibility(progress < 1 && !mHideActionsView);
         setClipChildren(!isFullscreen);
         setClipToPadding(!isFullscreen);
 
@@ -891,6 +937,12 @@
         invalidateOutline();
     }
 
+    private void updateActionsViewVisibility(boolean isVisible) {
+        if (mActionsView != null) {
+            mActionsView.setVisibility(isVisible ? VISIBLE : GONE);
+        }
+    }
+
     public boolean isRunningTask() {
         if (getRecentsView() == null) {
             return false;
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 60cfa0c..f9bb2f2 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -31,7 +31,6 @@
         android:id="@+id/icon"
         android:layout_width="@dimen/task_thumbnail_icon_size"
         android:layout_height="@dimen/task_thumbnail_icon_size"
-        android:layout_gravity="top|center_horizontal"
         android:focusable="false"
         android:importantForAccessibility="no"/>
 </com.android.quickstep.views.TaskView>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 07d2381..fa0e840 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.proxy.ProxyActivityStarter;
 import com.android.launcher3.proxy.StartActivityParams;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.uioverrides.BackButtonAlphaHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.launcher3.util.UiThreadHelper;
@@ -51,6 +52,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.RemoteFadeOutAnimationListener;
 import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.views.RecentsView;
 
 import java.util.stream.Stream;
 
@@ -210,9 +212,10 @@
     @Override
     protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
         if (SysUINavigationMode.getMode(this) == Mode.NO_BUTTON) {
-            float offscreenTranslationX = getDeviceProfile().widthPx
-                    - getOverviewPanel().getPaddingStart();
-            return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
+            PagedOrientationHandler layoutVertical =
+                ((RecentsView)getOverviewPanel()).getPagedViewOrientedState().getOrientationHandler();
+            return layoutVertical.getScaleAndTranslation(getDeviceProfile(),
+                getOverviewPanel());
         }
         return super.getOverviewScaleAndTranslationForNormalState();
     }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index fd55e07..58d6ee7 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.systemui.shared.recents.model.ThumbnailData;
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 3dae510..f78ac57 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -33,8 +33,11 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.states.RotationHelper;
 import com.android.launcher3.util.DefaultDisplay;
 
+import java.io.PrintWriter;
+
 /**
  * Maintains state for supporting nav bars and tracking their gestures in multiple orientations.
  * See {@link OrientationRectF#applyTransform(MotionEvent, boolean)} for transformation of
@@ -149,8 +152,7 @@
         Point size = display.realSize;
         int rotation = display.rotation;
         OrientationRectF orientationRectF =
-                new OrientationRectF(0, 0, size.x, size.y, rotation,
-                        size.y, size.x);
+                new OrientationRectF(0, 0, size.x, size.y, rotation);
         if (mMode == SysUINavigationMode.Mode.NO_BUTTON) {
             int touchHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
             orientationRectF.top = orientationRectF.bottom - touchHeight;
@@ -206,6 +208,14 @@
         return false;
     }
 
+    int getCurrentActiveRotation() {
+        if (mLastRectTouched == null) {
+            return 0;
+        } else {
+            return mLastRectTouched.mRotation;
+        }
+    }
+
     public void transform(MotionEvent event) {
         int eventAction = event.getActionMasked();
         switch (eventAction) {
@@ -249,6 +259,19 @@
         }
     }
 
+    public void dump(PrintWriter pw) {
+        pw.println("OrientationTouchTransformerState: ");
+        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
+        pw.println("  lastTouchedRegion=" + mLastRectTouched);
+        pw.println("  multipleRegionsEnabled=" + mEnableMultipleRegions);
+        StringBuilder regions = new StringBuilder("  currentTouchableRotations=");
+        for(int i = 0; i < mSwipeTouchRegions.size(); i++) {
+            OrientationRectF rectF = mSwipeTouchRegions.get(mSwipeTouchRegions.keyAt(i));
+            regions.append(rectF.mRotation).append(" ");
+        }
+        pw.println(regions.toString());
+    }
+
     private class OrientationRectF extends RectF {
 
         /**
@@ -262,12 +285,11 @@
         private float mHeight;
         private float mWidth;
 
-        OrientationRectF(float left, float top, float right, float bottom, int rotation,
-                float height, float width) {
+        OrientationRectF(float left, float top, float right, float bottom, int rotation) {
             super(left, top, right, bottom);
             this.mRotation = rotation;
-            mHeight = height - maxDelta;
-            mWidth = width - maxDelta;
+            mHeight = bottom - maxDelta;
+            mWidth = right - maxDelta;
         }
 
         @Override
@@ -280,7 +302,7 @@
         boolean applyTransform(MotionEvent event, boolean forceTransform) {
             MotionEvent tmp = MotionEvent.obtain(event);
             Matrix outMatrix = new Matrix();
-            int delta = deltaRotation(mCurrentRotation, mRotation);
+            int delta = RotationHelper.deltaRotation(mCurrentRotation, mRotation);
             switch (delta) {
                 case Surface.ROTATION_0:
                     outMatrix.reset();
@@ -314,16 +336,5 @@
             }
             return false;
         }
-
-        /**
-         * @return how many factors {@param newRotation} is rotated 90 degrees clockwise.
-         * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise...
-         * A value of 0 means no rotation has been applied
-         */
-        private int deltaRotation(int oldRotation, int newRotation) {
-            int delta = newRotation - oldRotation;
-            if (delta < 0) delta += 4;
-            return delta;
-        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 21a4918..8dd4aa4 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -39,7 +39,7 @@
 import java.util.function.Supplier;
 
 /**
- * Wrapper around RecentsAnimationController to help with some synchronization
+ * Wrapper around RecentsAnimationControllerCompat to help with some synchronization
  */
 public class RecentsAnimationController {
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 259d1dd..d845650 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -17,8 +17,6 @@
 
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 
-import static com.android.launcher3.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE;
-import static com.android.launcher3.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
@@ -40,13 +38,9 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.RectF;
 import android.graphics.Region;
 import android.os.Process;
 import android.os.UserManager;
-import android.graphics.Region;
-import android.os.Process;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -82,6 +76,7 @@
     private final SysUINavigationMode mSysUiNavMode;
     private final DefaultDisplay mDefaultDisplay;
     private final int mDisplayId;
+    private int mDisplayRotation;
 
     private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
 
@@ -234,6 +229,7 @@
             return;
         }
 
+        mDisplayRotation = info.rotation;
         mNavBarPosition = new NavBarPosition(mMode, info);
         updateGestureTouchRegions();
         mOrientationTouchTransformer.createOrAddTouchRegion(info);
@@ -499,6 +495,18 @@
         mOrientationTouchTransformer.transform(event);
     }
 
+    public void enableMultipleRegions(boolean enable) {
+        mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
+    }
+
+    public int getCurrentActiveRotation() {
+        return mOrientationTouchTransformer.getCurrentActiveRotation();
+    }
+
+    public int getDisplayRotation() {
+        return mDisplayRotation;
+    }
+
     public void dump(PrintWriter pw) {
         pw.println("DeviceState:");
         pw.println("  canStartSystemGesture=" + canStartSystemGesture());
@@ -508,9 +516,8 @@
         pw.println("  assistantAvailable=" + mAssistantAvailable);
         pw.println("  assistantDisabled="
                 + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
-    }
-
-    public void enableMultipleRegions(boolean enable) {
-        mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
+        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
+        pw.println("  displayRotation=" + getDisplayRotation());
+        mOrientationTouchTransformer.dump(pw);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 908747c..0210a81 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -334,25 +334,14 @@
         }
     }
 
-    @Override
-    public void setSplitScreenMinimized(boolean minimized) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.setSplitScreenMinimized(minimized);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call stopScreenPinning", e);
-            }
-        }
-    }
-
-    @Override
     public void onQuickSwitchToNewTask() {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.onQuickSwitchToNewTask();
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call onQuickstepStarted", e);
-            }
-        }
+        //TODO(b/150250451) add back in after big CL goes through
+//        if (mSystemUiProxy != null) {
+//            try {
+//                mSystemUiProxy.onQuickSwitchToNewTask();
+//            } catch (RemoteException e) {
+//                Log.w(TAG, "Failed call onQuickstepStarted", e);
+//            }
+//        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 1d8a79f..ba99016 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -33,6 +33,9 @@
 
 import java.lang.annotation.Retention;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 public class LayoutUtils {
 
     private static final int MULTI_WINDOW_STRATEGY_HALF_SCREEN = 1;
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 01893e9..ef6bd3d 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -16,11 +16,6 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
-import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
-import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
-
 import android.animation.LayoutTransition;
 import android.animation.TimeInterpolator;
 import android.annotation.SuppressLint;
@@ -35,6 +30,7 @@
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.Surface;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -49,13 +45,25 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.PagedViewOrientedState;
 import com.android.launcher3.pageindicators.PageIndicator;
+import com.android.launcher3.touch.PortraitPagedViewHandler;
 import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds;
 import com.android.launcher3.util.OverScroller;
 import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
 
+import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
+import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
+import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE;
+import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_BY;
+import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_TO;
+
 /**
  * An abstraction of the original Workspace which supports browsing through a
  * sequential list of "pages"
@@ -64,6 +72,8 @@
     private static final String TAG = "PagedView";
     private static final boolean DEBUG = false;
 
+    public static boolean sFlagForcedRotation = false;
+
     public static final int INVALID_PAGE = -1;
     protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE;
 
@@ -97,8 +107,8 @@
 
     @ViewDebug.ExportedProperty(category = "launcher")
     protected int mNextPage = INVALID_PAGE;
-    protected int mMinScrollX;
-    protected int mMaxScrollX;
+    protected int mMaxScroll;
+    protected int mMinScroll;
     protected OverScroller mScroller;
     private Interpolator mDefaultInterpolator;
     private VelocityTracker mVelocityTracker;
@@ -106,9 +116,12 @@
 
     private float mDownMotionX;
     private float mDownMotionY;
-    private float mLastMotionX;
-    private float mLastMotionXRemainder;
-    private float mTotalMotionX;
+    private float mDownMotionPrimary;
+    private float mLastMotion;
+    private float mLastMotionRemainder;
+    private float mTotalMotion;
+    protected PagedOrientationHandler mOrientationHandler = new PortraitPagedViewHandler();
+    protected final PagedViewOrientedState mOrientationState = new PagedViewOrientedState();
 
     protected int[] mPageScrolls;
     private boolean mIsBeingDragged;
@@ -123,11 +136,14 @@
 
     protected boolean mIsPageInTransition = false;
 
-    protected float mSpringOverScrollX;
+    protected float mSpringOverScroll;
 
     protected boolean mWasInOverscroll = false;
 
-    protected int mUnboundedScrollX;
+    protected int mUnboundedScroll;
+
+    protected int mLayoutRotation = Surface.ROTATION_0;
+    protected int mDisplayRotation = Surface.ROTATION_0;
 
     // Page Indicator
     @Thunk int mPageIndicatorViewId;
@@ -166,11 +182,12 @@
      * Initializes various states for this workspace.
      */
     protected void init() {
-        mScroller = new OverScroller(getContext());
+        Context context = getContext();
+        mScroller = new OverScroller(context);
         setDefaultInterpolator(Interpolators.SCROLL);
         mCurrentPage = 0;
 
-        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
         mTouchSlop = configuration.getScaledPagingTouchSlop();
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
 
@@ -182,6 +199,8 @@
         if (Utilities.ATLEAST_OREO) {
             setDefaultFocusHighlightEnabled(false);
         }
+
+        sFlagForcedRotation = Utilities.isForcedRotation(context);
     }
 
     protected void setDefaultInterpolator(Interpolator interpolator) {
@@ -236,12 +255,12 @@
      */
     protected void updateCurrentPageScroll() {
         // If the current page is invalid, just reset the scroll position to zero
-        int newX = 0;
+        int newPosition = 0;
         if (0 <= mCurrentPage && mCurrentPage < getPageCount()) {
-            newX = getScrollForPage(mCurrentPage);
+            newPosition = getScrollForPage(mCurrentPage);
         }
-        scrollTo(newX, 0);
-        mScroller.startScroll(mScroller.getCurrPos(), newX - mScroller.getCurrPos());
+        mOrientationHandler.set(this, VIEW_SCROLL_TO, newPosition);
+        mOrientationHandler.scrollerStartScroll(mScroller, newPosition);
         forceFinishScroller(true);
     }
 
@@ -285,7 +304,7 @@
         int dir = !mIsRtl ? 1 : - 1;
         int currScroll = getScrollForPage(page);
         int prevScroll;
-        while (currScroll < mMinScrollX) {
+        while (currScroll < mMinScroll) {
             page += dir;
             prevScroll = currScroll;
             currScroll = getScrollForPage(page);
@@ -294,7 +313,7 @@
                 break;
             }
         }
-        while (currScroll > mMaxScrollX) {
+        while (currScroll > mMaxScroll) {
             page -= dir;
             prevScroll = currScroll;
             currScroll = getScrollForPage(page);
@@ -378,45 +397,73 @@
                 AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
     }
 
-    protected int getUnboundedScrollX() {
-        return mUnboundedScrollX;
+    protected int getUnboundedScroll() {
+        return mUnboundedScroll;
+    }
+
+    protected void updateLayoutRotation(int touchRotation) {
+        setLayoutRotation(touchRotation, mDisplayRotation);
+    }
+
+    /** @param touchRotation Must be one of {@link android.view.Surface.ROTATION_0/90/180/270} */
+    public void setLayoutRotation(int touchRotation, int displayRotation) {
+        if (mLayoutRotation == touchRotation && mDisplayRotation == displayRotation) {
+            return;
+        }
+
+        mOrientationState.update(touchRotation, displayRotation);
+        mOrientationHandler = mOrientationState.getOrientationHandler();
+        mLayoutRotation = touchRotation;
+        mDisplayRotation = displayRotation;
+        requestLayout();
+    }
+
+    public PagedViewOrientedState getPagedViewOrientedState() {
+        return mOrientationState;
+    }
+
+    public PagedOrientationHandler getPagedOrientationHandler() {
+        return getPagedViewOrientedState().getOrientationHandler();
+    }
+
+    public void disableMultipleLayoutRotations(boolean disable) {
+        mOrientationState.disableMultipleOrientations(disable);
+        mOrientationHandler = mOrientationState.getOrientationHandler();
+        requestLayout();
     }
 
     @Override
     public void scrollBy(int x, int y) {
-        scrollTo(getUnboundedScrollX() + x, getScrollY() + y);
+        mOrientationHandler.delegateScrollBy(this, getUnboundedScroll(), x, y);
     }
 
     @Override
     public void scrollTo(int x, int y) {
-        mUnboundedScrollX = x;
+        int primaryScroll = mOrientationHandler.getPrimaryValue(x, y);
+        int secondaryScroll = mOrientationHandler.getSecondaryValue(x, y);
+        mUnboundedScroll = primaryScroll;
 
-        boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < mMinScrollX);
-        boolean isXAfterLastPage = mIsRtl ? (x < mMinScrollX) : (x > mMaxScrollX);
-
-        if (!isXBeforeFirstPage && !isXAfterLastPage) {
-            mSpringOverScrollX = 0;
+        boolean isBeforeFirstPage = mIsRtl ?
+            (primaryScroll > mMaxScroll) : (primaryScroll < mMinScroll);
+        boolean isAfterLastPage = mIsRtl ?
+            (primaryScroll < mMinScroll) : (primaryScroll > mMaxScroll);
+        if (!isBeforeFirstPage && !isAfterLastPage) {
+            mSpringOverScroll = 0;
         }
 
-        if (isXBeforeFirstPage) {
-            super.scrollTo(mIsRtl ? mMaxScrollX : mMinScrollX, y);
+        if (isBeforeFirstPage) {
+            mOrientationHandler.delegateScrollTo(this,
+                secondaryScroll, mIsRtl ? mMaxScroll : mMinScroll);
             if (mAllowOverScroll) {
                 mWasInOverscroll = true;
-                if (mIsRtl) {
-                    overScroll(x - mMaxScrollX);
-                } else {
-                    overScroll(x - mMinScrollX);
-                }
+                overScroll(primaryScroll - (mIsRtl ? mMaxScroll : mMinScroll));
             }
-        } else if (isXAfterLastPage) {
-            super.scrollTo(mIsRtl ? mMinScrollX : mMaxScrollX, y);
+        } else if (isAfterLastPage) {
+            mOrientationHandler.delegateScrollTo(this,
+                secondaryScroll, mIsRtl ? mMinScroll : mMaxScroll);
             if (mAllowOverScroll) {
                 mWasInOverscroll = true;
-                if (mIsRtl) {
-                    overScroll(x - mMinScrollX);
-                } else {
-                    overScroll(x - mMaxScrollX);
-                }
+                overScroll(primaryScroll - (mIsRtl ? mMinScroll : mMaxScroll));
             }
         } else {
             if (mWasInOverscroll) {
@@ -425,7 +472,13 @@
             }
             super.scrollTo(x, y);
         }
+    }
 
+    /**
+     * Helper for {@link PagedOrientationHandler} to be able to call parent's scrollTo method
+     */
+    public void superScrollTo(int x, int y) {
+        super.scrollTo(x, y);
     }
 
     private void sendScrollAccessibilityEvent() {
@@ -436,9 +489,7 @@
                 ev.setScrollable(true);
                 ev.setScrollX(getScrollX());
                 ev.setScrollY(getScrollY());
-                ev.setMaxScrollX(mMaxScrollX);
-                ev.setMaxScrollY(0);
-
+                mOrientationHandler.setMaxScroll(ev, mMaxScroll);
                 sendAccessibilityEventUnchecked(ev);
             }
         }
@@ -459,9 +510,10 @@
     protected boolean computeScrollHelper(boolean shouldInvalidate) {
         if (mScroller.computeScrollOffset()) {
             // Don't bother scrolling if the page does not need to be moved
-            if (getUnboundedScrollX() != mScroller.getCurrPos()
-                    || getScrollX() != mScroller.getCurrPos()) {
-                scrollTo(mScroller.getCurrPos(), 0);
+            int currentScroll = mOrientationHandler.getPrimaryScroll(this);
+            if (mUnboundedScroll != mScroller.getCurrPos()
+                || currentScroll != mScroller.getCurrPos()) {
+                mOrientationHandler.set(this, VIEW_SCROLL_TO, mScroller.getCurrPos());
             }
             if (shouldInvalidate) {
                 invalidate();
@@ -580,7 +632,8 @@
 
         if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
 
-        if (getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC)) {
+        boolean isScrollChanged = getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC);
+        if (isScrollChanged) {
             pageScrollChanged = true;
         }
 
@@ -621,7 +674,6 @@
     /**
      * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length
      * of {@code outPageScrolls} should be same as the the childCount
-     *
      */
     protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
             ComputePageScrollsLogic scrollLogic) {
@@ -631,36 +683,30 @@
         final int endIndex = mIsRtl ? -1 : childCount;
         final int delta = mIsRtl ? -1 : 1;
 
-        final int verticalCenter = (getPaddingTop() + getMeasuredHeight() + mInsets.top
-                - mInsets.bottom - getPaddingBottom()) / 2;
+        final int pageCenter = mOrientationHandler.getCenterForPage(this, mInsets);
 
-        final int scrollOffsetLeft = mInsets.left + getPaddingLeft();
-        final int scrollOffsetRight = getWidth() - getPaddingRight() - mInsets.right;
+        final int scrollOffsetStart = mOrientationHandler.getScrollOffsetStart(this, mInsets);
+        final int scrollOffsetEnd = mOrientationHandler.getScrollOffsetEnd(this, mInsets);
         boolean pageScrollChanged = false;
 
-        for (int i = startIndex, childLeft = scrollOffsetLeft; i != endIndex; i += delta) {
+        for (int i = startIndex, childStart = scrollOffsetStart; i != endIndex; i += delta) {
             final View child = getPageAt(i);
             if (scrollLogic.shouldIncludeView(child)) {
-                final int childWidth = child.getMeasuredWidth();
-                final int childRight = childLeft + childWidth;
-
-                if (layoutChildren) {
-                    final int childHeight = child.getMeasuredHeight();
-                    final int childTop = verticalCenter - childHeight / 2;
-                    child.layout(childLeft, childTop, childRight, childTop + childHeight);
-                }
+                ChildBounds bounds = mOrientationHandler.getChildBounds(child, childStart,
+                    pageCenter, layoutChildren);
+                final int primaryDimension = bounds.primaryDimension;
+                final int childPrimaryEnd = bounds.childPrimaryEnd;
 
                 // In case the pages are of different width, align the page to left or right edge
                 // based on the orientation.
                 final int pageScroll = mIsRtl
-                        ? (childLeft - scrollOffsetLeft)
-                        : Math.max(0, childRight  - scrollOffsetRight);
+                    ? (childStart - scrollOffsetStart)
+                    : Math.max(0, childPrimaryEnd  - scrollOffsetEnd);
                 if (outPageScrolls[i] != pageScroll) {
                     pageScrollChanged = true;
                     outPageScrolls[i] = pageScroll;
                 }
-
-                childLeft += childWidth + mPageSpacing + getChildGap();
+                childStart += primaryDimension + mPageSpacing + getChildGap();
             }
         }
         return pageScrollChanged;
@@ -671,15 +717,15 @@
     }
 
     protected void updateMinAndMaxScrollX() {
-        mMinScrollX = computeMinScrollX();
-        mMaxScrollX = computeMaxScrollX();
+        mMinScroll = computeMinScroll();
+        mMaxScroll = computeMaxScroll();
     }
 
-    protected int computeMinScrollX() {
+    protected int computeMinScroll() {
         return 0;
     }
 
-    protected int computeMaxScrollX() {
+    protected int computeMaxScroll() {
         int childCount = getChildCount();
         if (childCount > 0) {
             final int index = mIsRtl ? 0 : childCount - 1;
@@ -722,7 +768,8 @@
 
     protected int getChildOffset(int index) {
         if (index < 0 || index > getChildCount() - 1) return 0;
-        return getPageAt(index).getLeft();
+        View pageAtIndex = getPageAt(index);
+        return mOrientationHandler.getChildStart(pageAtIndex);
     }
 
     @Override
@@ -873,13 +920,13 @@
             case MotionEvent.ACTION_MOVE: {
                 /*
                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
-                 * whether the user has moved far enough from his original down touch.
+                 * whether the user has moved far enough from their original down touch.
                  */
                 if (mActivePointerId != INVALID_POINTER) {
                     determineScrollingStart(ev);
                 }
                 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
-                // event. in that case, treat the first occurence of a move event as a ACTION_DOWN
+                // event. in that case, treat the first occurrence of a move event as a ACTION_DOWN
                 // i.e. fall through to the next case (don't break)
                 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
                 // while it's small- this was causing a crash before we checked for INVALID_POINTER)
@@ -892,9 +939,9 @@
                 // Remember location of down touch
                 mDownMotionX = x;
                 mDownMotionY = y;
-                mLastMotionX = x;
-                mLastMotionXRemainder = 0;
-                mTotalMotionX = 0;
+                mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
+                mLastMotionRemainder = 0;
+                mTotalMotion = 0;
                 mActivePointerId = ev.getPointerId(0);
 
                 updateIsBeingDraggedOnTouchDown();
@@ -956,17 +1003,17 @@
         final int pointerIndex = ev.findPointerIndex(mActivePointerId);
         if (pointerIndex == -1) return;
 
-        final float x = ev.getX(pointerIndex);
-        final int xDiff = (int) Math.abs(x - mLastMotionX);
+        final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
+        final int diff = (int) Math.abs(primaryDirection - mLastMotion);
         final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
-        boolean xMoved = xDiff > touchSlop;
+        boolean moved = diff > touchSlop;
 
-        if (xMoved) {
+        if (moved) {
             // Scroll if the user moved far enough along the X axis
             mIsBeingDragged = true;
-            mTotalMotionX += Math.abs(mLastMotionX - x);
-            mLastMotionX = x;
-            mLastMotionXRemainder = 0;
+            mTotalMotion += Math.abs(mLastMotion - primaryDirection);
+            mLastMotion = primaryDirection;
+            mLastMotionRemainder = 0;
             onScrollInteractionBegin();
             pageBeginTransition();
             // Stop listening for things like pinches.
@@ -1033,10 +1080,9 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
-        if (mScroller.isSpringing() && mSpringOverScrollX != 0) {
+        if (mScroller.isSpringing() && mSpringOverScroll != 0) {
             int saveCount = canvas.save();
-
-            canvas.translate(-mSpringOverScrollX, 0);
+            mOrientationHandler.set(canvas, CANVAS_TRANSLATE, -mSpringOverScroll);
             super.dispatchDraw(canvas);
 
             canvas.restoreToCount(saveCount);
@@ -1046,25 +1092,27 @@
     }
 
     protected void dampedOverScroll(int amount) {
-        mSpringOverScrollX = amount;
+        mSpringOverScroll = amount;
         if (amount == 0) {
             return;
         }
 
-        int overScrollAmount = OverScroll.dampedScroll(amount, getMeasuredWidth());
-        mSpringOverScrollX = overScrollAmount;
+        int size = mOrientationHandler.getMeasuredSize(this);
+        int overScrollAmount = OverScroll.dampedScroll(amount, size);
+        mSpringOverScroll = overScrollAmount;
         if (mScroller.isSpringing()) {
             invalidate();
             return;
         }
 
-        int x = Utilities.boundToRange(getScrollX(), mMinScrollX, mMaxScrollX);
-        super.scrollTo(x + overScrollAmount, getScrollY());
+        int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+        int boundedScroll = Utilities.boundToRange(primaryScroll, mMinScroll, mMaxScroll);
+        mOrientationHandler.delegateScrollTo(this, boundedScroll + overScrollAmount);
         invalidate();
     }
 
     protected void overScroll(int amount) {
-        mSpringOverScrollX = amount;
+        mSpringOverScroll = amount;
         if (mScroller.isSpringing()) {
             invalidate();
             return;
@@ -1073,11 +1121,8 @@
         if (amount == 0) return;
 
         if (mFreeScroll && !mScroller.isFinished()) {
-            if (amount < 0) {
-                super.scrollTo(mMinScrollX + amount, getScrollY());
-            } else {
-                super.scrollTo(mMaxScrollX + amount, getScrollY());
-            }
+            int scrollAmount = amount < 0 ? mMinScroll + amount : mMaxScroll + amount;
+            mOrientationHandler.delegateScrollTo(this, scrollAmount);
         } else {
             dampedOverScroll(amount);
         }
@@ -1127,37 +1172,37 @@
             }
 
             // Remember where the motion event started
-            mDownMotionX = mLastMotionX = ev.getX();
+            mDownMotionX = ev.getX();
             mDownMotionY = ev.getY();
-            mLastMotionXRemainder = 0;
-            mTotalMotionX = 0;
+            mDownMotionPrimary = mLastMotion = mOrientationHandler.getPrimaryDirection(ev, 0);
+            mLastMotionRemainder = 0;
+            mTotalMotion = 0;
             mActivePointerId = ev.getPointerId(0);
-
             if (mIsBeingDragged) {
                 onScrollInteractionBegin();
                 pageBeginTransition();
             }
             break;
 
-        case MotionEvent.ACTION_MOVE:
-            if (mIsBeingDragged) {
+            case MotionEvent.ACTION_MOVE:
+                if (mIsBeingDragged) {
                 // Scroll to follow the motion event
                 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
 
                 if (pointerIndex == -1) return true;
 
-                final float x = ev.getX(pointerIndex);
-                final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
-
-                mTotalMotionX += Math.abs(deltaX);
+                float direction = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
+                float delta = mLastMotion + mLastMotionRemainder - direction;
+                mTotalMotion += Math.abs(delta);
 
                 // Only scroll and update mLastMotionX if we have moved some discrete amount.  We
                 // keep the remainder because we are actually testing if we've moved from the last
                 // scrolled position (which is discrete).
-                if (Math.abs(deltaX) >= 1.0f) {
-                    scrollBy((int) deltaX, 0);
-                    mLastMotionX = x;
-                    mLastMotionXRemainder = deltaX - (int) deltaX;
+                if (Math.abs(delta) >= 1.0f) {
+                    mLastMotion = direction;
+                    mLastMotionRemainder = delta - (int) delta;
+
+                    mOrientationHandler.set(this, VIEW_SCROLL_BY, (int) delta);
                 } else {
                     awakenScrollBars();
                 }
@@ -1170,27 +1215,31 @@
             if (mIsBeingDragged) {
                 final int activePointerId = mActivePointerId;
                 final int pointerIndex = ev.findPointerIndex(activePointerId);
-                final float x = ev.getX(pointerIndex);
+                final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev,
+                    pointerIndex);
                 final VelocityTracker velocityTracker = mVelocityTracker;
                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-                int velocityX = (int) velocityTracker.getXVelocity(mActivePointerId);
-                final int deltaX = (int) (x - mDownMotionX);
-                final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth();
-                boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
-                        SIGNIFICANT_MOVE_THRESHOLD;
 
-                mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x);
-                boolean isFling = mTotalMotionX > mTouchSlop && shouldFlingForVelocity(velocityX);
-                boolean isDeltaXLeft = mIsRtl ? deltaX > 0 : deltaX < 0;
-                boolean isVelocityXLeft = mIsRtl ? velocityX > 0 : velocityX < 0;
+                int velocity = (int) mOrientationHandler.getPrimaryVelocity(velocityTracker,
+                    mActivePointerId);
+                int delta = (int) (primaryDirection - mDownMotionPrimary);
+                int pageOrientedSize = mOrientationHandler.getMeasuredSize(getPageAt(mCurrentPage));
+
+                boolean isSignificantMove = Math.abs(delta) > pageOrientedSize *
+                    SIGNIFICANT_MOVE_THRESHOLD;
+
+                mTotalMotion += Math.abs(mLastMotion + mLastMotionRemainder - primaryDirection);
+                boolean isFling = mTotalMotion > mTouchSlop && shouldFlingForVelocity(velocity);
+                boolean isDeltaLeft = mIsRtl ? delta > 0 : delta < 0;
+                boolean isVelocityLeft = mIsRtl ? velocity > 0 : velocity < 0;
 
                 if (!mFreeScroll) {
                     // In the case that the page is moved far to one direction and then is flung
                     // in the opposite direction, we use a threshold to determine whether we should
                     // just return to the starting page, or if we should skip one further.
                     boolean returnToOriginalPage = false;
-                    if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
-                            Math.signum(velocityX) != Math.signum(deltaX) && isFling) {
+                    if (Math.abs(delta) > pageOrientedSize * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
+                            Math.signum(velocity) != Math.signum(delta) && isFling) {
                         returnToOriginalPage = true;
                     }
 
@@ -1199,15 +1248,15 @@
                     // test for a large move if a fling has been registered. That is, a large
                     // move to the left and fling to the right will register as a fling to the right.
 
-                    if (((isSignificantMove && !isDeltaXLeft && !isFling) ||
-                            (isFling && !isVelocityXLeft)) && mCurrentPage > 0) {
+                    if (((isSignificantMove && !isDeltaLeft && !isFling) ||
+                            (isFling && !isVelocityLeft)) && mCurrentPage > 0) {
                         finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
-                        snapToPageWithVelocity(finalPage, velocityX);
-                    } else if (((isSignificantMove && isDeltaXLeft && !isFling) ||
-                            (isFling && isVelocityXLeft)) &&
+                        snapToPageWithVelocity(finalPage, velocity);
+                    } else if (((isSignificantMove && isDeltaLeft && !isFling) ||
+                            (isFling && isVelocityLeft)) &&
                             mCurrentPage < getChildCount() - 1) {
                         finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
-                        snapToPageWithVelocity(finalPage, velocityX);
+                        snapToPageWithVelocity(finalPage, velocity);
                     } else {
                         snapToDestination();
                     }
@@ -1216,38 +1265,40 @@
                         abortScrollerAnimation(true);
                     }
 
-                    int initialScrollX = getScrollX();
+                    int initialScroll = mOrientationHandler.getPrimaryScroll(this);
+                    int maxScroll = mMaxScroll;
+                    int minScroll = mMinScroll;
 
-                    if (((initialScrollX >= mMaxScrollX) && (isVelocityXLeft || !isFling)) ||
-                            ((initialScrollX <= mMinScrollX) && (!isVelocityXLeft || !isFling))) {
-                        mScroller.springBack(getScrollX(), mMinScrollX, mMaxScrollX);
+                    if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) ||
+                        ((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) {
+                        mScroller.springBack(initialScroll, minScroll, maxScroll);
                         mNextPage = getPageNearestToCenterOfScreen();
                     } else {
                         mScroller.setInterpolator(mDefaultInterpolator);
-                        mScroller.fling(initialScrollX, -velocityX,
-                                mMinScrollX, mMaxScrollX,
-                                Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
+                        mScroller.fling(initialScroll, -velocity,
+                            minScroll, maxScroll,
+                            Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
 
-                        int finalX = mScroller.getFinalPos();
-                        mNextPage = getPageNearestToCenterOfScreen(finalX);
+                        int finalPos = mScroller.getFinalPos();
+                        mNextPage = getPageNearestToCenterOfScreen(finalPos);
 
                         int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
                         int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
-                        if (finalX > mMinScrollX && finalX < mMaxScrollX) {
+                        if (finalPos > minScroll && finalPos < maxScroll) {
                             // If scrolling ends in the half of the added space that is closer to
                             // the end, settle to the end. Otherwise snap to the nearest page.
                             // If flinging past one of the ends, don't change the velocity as it
                             // will get stopped at the end anyway.
-                            int pageSnappedX = finalX < (firstPageScroll + mMinScrollX) / 2
-                                    ? mMinScrollX
-                                    : finalX > (lastPageScroll + mMaxScrollX) / 2
-                                            ? mMaxScrollX
-                                            : getScrollForPage(mNextPage);
+                            int pageSnapped = finalPos < (firstPageScroll + minScroll) / 2
+                                ? minScroll
+                                : finalPos > (lastPageScroll + maxScroll) / 2
+                                    ? maxScroll
+                                    : getScrollForPage(mNextPage);
 
-                            mScroller.setFinalPos(pageSnappedX);
+                            mScroller.setFinalPos(pageSnapped);
                             // Ensure the scroll/snap doesn't happen too fast;
                             int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
-                                    - mScroller.getDuration();
+                                - mScroller.getDuration();
                             if (extraScrollDuration > 0) {
                                 mScroller.extendDuration(extraScrollDuration);
                             }
@@ -1279,8 +1330,8 @@
         return true;
     }
 
-    protected boolean shouldFlingForVelocity(int velocityX) {
-        return Math.abs(velocityX) > mFlingThresholdVelocity;
+    protected boolean shouldFlingForVelocity(int velocity) {
+        return Math.abs(velocity) > mFlingThresholdVelocity;
     }
 
     private void resetTouchState() {
@@ -1364,8 +1415,9 @@
             // active pointer and adjust accordingly.
             // TODO: Make this decision more intelligent.
             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
-            mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
-            mLastMotionXRemainder = 0;
+            mLastMotion = mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev,
+                newPointerIndex);
+            mLastMotionRemainder = 0;
             mActivePointerId = ev.getPointerId(newPointerIndex);
             if (mVelocityTracker != null) {
                 mVelocityTracker.clear();
@@ -1383,19 +1435,20 @@
     }
 
     public int getPageNearestToCenterOfScreen() {
-        return getPageNearestToCenterOfScreen(getScrollX());
+        return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this));
     }
 
-    private int getPageNearestToCenterOfScreen(int scaledScrollX) {
-        int screenCenter = scaledScrollX + (getMeasuredWidth() / 2);
+    private int getPageNearestToCenterOfScreen(int scaledScroll) {
+        int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
+        int screenCenter = scaledScroll + (pageOrientationSize / 2);
         int minDistanceFromScreenCenter = Integer.MAX_VALUE;
         int minDistanceFromScreenCenterIndex = -1;
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; ++i) {
             View layout = getPageAt(i);
-            int childWidth = layout.getMeasuredWidth();
-            int halfChildWidth = (childWidth / 2);
-            int childCenter = getChildOffset(i) + halfChildWidth;
+            int childSize = mOrientationHandler.getMeasuredSize(layout);
+            int halfChildSize = (childSize / 2);
+            int childCenter = getChildOffset(i) + halfChildSize;
             int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
             if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
                 minDistanceFromScreenCenter = distanceFromScreenCenter;
@@ -1410,7 +1463,8 @@
     }
 
     protected boolean isInOverScroll() {
-        return (getScrollX() > mMaxScrollX || getScrollX() < mMinScrollX);
+        int scroll = mOrientationHandler.getPrimaryScroll(this);
+        return scroll > mMaxScroll || scroll < mMinScroll;
     }
 
     protected int getPageSnapDuration() {
@@ -1432,10 +1486,10 @@
 
     protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
         whichPage = validateNewPage(whichPage);
-        int halfScreenSize = getMeasuredWidth() / 2;
+        int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2;
 
-        final int newX = getScrollForPage(whichPage);
-        int delta = newX - getUnboundedScrollX();
+        final int newLoc = getScrollForPage(whichPage);
+        int delta = newLoc - getUnboundedScroll();
         int duration = 0;
 
         if (Math.abs(velocity) < mMinFlingVelocity) {
@@ -1462,7 +1516,7 @@
 
         if (QUICKSTEP_SPRINGS.get()) {
             return snapToPage(whichPage, delta, duration, false, null,
-                    velocity * Math.signum(newX - getUnboundedScrollX()), true);
+                    velocity * Math.signum(delta), true);
         } else {
             return snapToPage(whichPage, delta, duration);
         }
@@ -1488,8 +1542,8 @@
             TimeInterpolator interpolator) {
         whichPage = validateNewPage(whichPage);
 
-        int newX = getScrollForPage(whichPage);
-        final int delta = newX - getUnboundedScrollX();
+        int newLoc = getScrollForPage(whichPage);
+        final int delta = newLoc - getUnboundedScroll();
         return snapToPage(whichPage, delta, duration, immediate, interpolator, 0, false);
     }
 
@@ -1535,9 +1589,9 @@
         }
 
         if (spring && QUICKSTEP_SPRINGS.get()) {
-            mScroller.startScrollSpring(getUnboundedScrollX(), delta, duration, velocity);
+            mScroller.startScrollSpring(getUnboundedScroll(), delta, duration, velocity);
         } else {
-            mScroller.startScroll(getUnboundedScrollX(), delta, duration);
+            mScroller.startScroll(getUnboundedScroll(), delta, duration);
         }
 
         updatePageIndicator();
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index e0e4cc0..9780630 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_ICON_BADGED;
+import static com.android.launcher3.states.RotationHelper.FIXED_ROTATION_TRANSFORM_SETTING_NAME;
 
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
@@ -127,6 +128,11 @@
                         Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
     }
 
+    public static boolean isForcedRotation(Context context) {
+        return Settings.Global.getInt(context.getContentResolver(),
+            FIXED_ROTATION_TRANSFORM_SETTING_NAME, 0) != 0;
+    }
+
     // An intent extra to indicate the horizontal scroll of the wallpaper.
     public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET";
     public static final String EXTRA_WALLPAPER_FLAVOR = "com.android.launcher3.WALLPAPER_FLAVOR";
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index a8f492f..590c620 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -966,8 +966,8 @@
 
     private boolean isScrollingOverlay() {
         return mLauncherOverlay != null &&
-                ((mIsRtl && getUnboundedScrollX() > mMaxScrollX)
-                        || (!mIsRtl && getUnboundedScrollX() < mMinScrollX));
+                ((mIsRtl && getUnboundedScroll() > mMaxScroll)
+                        || (!mIsRtl && getUnboundedScroll() < mMinScroll));
     }
 
     @Override
@@ -1003,7 +1003,7 @@
 
     public void showPageIndicatorAtCurrentScroll() {
         if (mPageIndicator != null) {
-            mPageIndicator.setScroll(getScrollX(), computeMaxScrollX());
+            mPageIndicator.setScroll(getScrollX(), computeMaxScroll());
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsPagedView.java b/src/com/android/launcher3/allapps/AllAppsPagedView.java
index 5b73940..ab4cb6b 100644
--- a/src/com/android/launcher3/allapps/AllAppsPagedView.java
+++ b/src/com/android/launcher3/allapps/AllAppsPagedView.java
@@ -48,7 +48,7 @@
     @Override
     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
         super.onScrollChanged(l, t, oldl, oldt);
-        mPageIndicator.setScroll(l, mMaxScrollX);
+        mPageIndicator.setScroll(l, mMaxScroll);
     }
 
     @Override
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 3b5fd59..c6d62f8 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -258,7 +258,7 @@
     @Override
     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
         super.onScrollChanged(l, t, oldl, oldt);
-        mPageIndicator.setScroll(l, mMaxScrollX);
+        mPageIndicator.setScroll(l, mMaxScroll);
     }
 
     /**
diff --git a/src/com/android/launcher3/model/PagedViewOrientedState.java b/src/com/android/launcher3/model/PagedViewOrientedState.java
new file mode 100644
index 0000000..fd1154c
--- /dev/null
+++ b/src/com/android/launcher3/model/PagedViewOrientedState.java
@@ -0,0 +1,100 @@
+/*
+ *
+ *  * 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.
+ *
+ */
+
+package com.android.launcher3.model;
+
+import android.view.Surface;
+
+import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.touch.PortraitPagedViewHandler;
+import com.android.launcher3.touch.LandscapePagedViewHandler;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.touch.SeascapePagedViewHandler;
+
+/**
+ * Container to hold orientation/rotation related information for Launcher.
+ * This is not meant to be an abstraction layer for applying different functionality between
+ * the different orientation/rotations. For that see {@link PagedOrientationHandler}
+ *
+ * This class has initial default state assuming the device and foreground app have
+ * no ({@link Surface.ROTATION_0} rotation.
+ *
+ * Currently this class resides in {@link com.android.launcher3.PagedView}, but there's a ticket
+ * to disassociate it from Launcher since it's needed before Launcher is instantiated
+ * See TODO(b/150300347)
+ */
+public final class PagedViewOrientedState {
+
+    private PagedOrientationHandler mOrientationHandler = new PortraitPagedViewHandler();
+
+    private int mTouchRotation = Surface.ROTATION_0;
+    private int mDisplayRotation = Surface.ROTATION_0;
+    /**
+     * If {@code true} we default to {@link PortraitPagedViewHandler} and don't support any fake
+     * launcher orientations.
+     */
+    private boolean mDisableMultipleOrientations;
+
+    public void update(int touchRotation, int displayRotation) {
+        mDisplayRotation = displayRotation;
+        mTouchRotation = touchRotation;
+        if (mTouchRotation == Surface.ROTATION_90) {
+            mOrientationHandler = new LandscapePagedViewHandler();
+        } else if (mTouchRotation == Surface.ROTATION_270) {
+            mOrientationHandler = new SeascapePagedViewHandler();
+        } else {
+            mOrientationHandler = new PortraitPagedViewHandler();
+        }
+    }
+
+    /**
+     * @return {@code true} if the area where the user touched the nav bar is the expected
+     * location for the given display rotation. Ex. bottom of phone in portrait, or left side of
+     * phone in landscape, right side in seascape, etc.
+     * False otherwise
+     */
+    public boolean isTouchRegionNaturalForDisplay() {
+        return mTouchRotation == mDisplayRotation;
+    }
+
+    public boolean areMultipleLayoutOrientationsDisabled() {
+        return mDisableMultipleOrientations;
+    }
+
+    public void disableMultipleOrientations(boolean disable) {
+        mDisableMultipleOrientations = disable;
+        if (disable) {
+            mOrientationHandler = new PortraitPagedViewHandler();
+        }
+    }
+
+    /**
+     * Gets the difference between the rotation of the device/display and which region the
+     * user is currently interacting with in factors of 90 degree clockwise rotations.
+     * Ex. Display is in portrait -> 0, user touches landscape region -> 1, this
+     * method would return 3 because it takes 3 clockwise 90 degree rotations from normal to
+     * landscape (portrait -> seascape -> reverse portrait -> landscape)
+     */
+    public int getTouchDisplayDelta() {
+        return RotationHelper.deltaRotation(mTouchRotation, mDisplayRotation);
+    }
+
+    public PagedOrientationHandler getOrientationHandler() {
+        return mOrientationHandler;
+    }
+}
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index 812268a..b193ffd 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -176,7 +176,7 @@
 
     // SingleAxisSwipeDetector.Listener's
     @Override
-    public void onDragStart(boolean start) { }
+    public void onDragStart(boolean start, float startDisplacement) { }
 
 
     @Override
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 852928b..95b13b4 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -17,15 +17,26 @@
 
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.content.ContentResolver;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.provider.Settings;
+import android.view.MotionEvent;
+import android.view.Surface;
 import android.view.WindowManager;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
@@ -38,6 +49,8 @@
 
     public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
 
+    public static final String FIXED_ROTATION_TRANSFORM_SETTING_NAME = "fixed_rotation_transform";
+
     public static boolean getAllowRotationDefaultValue() {
         // If the device was scaled, used the original dimensions to determine if rotation
         // is allowed of not.
@@ -92,6 +105,18 @@
         } else {
             mPrefs = null;
         }
+
+        // TODO(b/150260456) Add this in home settings as well
+        final ContentResolver resolver = launcher.getContentResolver();
+        final ContentObserver observer = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
+            @Override
+            public void onChange(boolean selfChange) {
+                PagedView.sFlagForcedRotation = Utilities.isForcedRotation(mLauncher);
+            }
+        };
+        resolver.registerContentObserver(Settings.Global.getUriFor(
+            FIXED_ROTATION_TRANSFORM_SETTING_NAME),
+            false, observer);
     }
 
     public void setRotationHadDifferentUI(boolean rotationHasDifferentUI) {
@@ -204,6 +229,118 @@
         }
     }
 
+    public static int getDegreesFromRotation(int rotation) {
+        int degrees;
+        switch (rotation) {
+            case Surface.ROTATION_90:
+                degrees = 90;
+                break;
+            case Surface.ROTATION_180:
+                degrees = 180;
+                break;
+            case Surface.ROTATION_270:
+                degrees = 270;
+                break;
+            case Surface.ROTATION_0:
+            default:
+                degrees = 0;
+                break;
+        }
+        return degrees;
+    }
+
+    public static int getRotationFromDegrees(int degrees, int currentRotation) {
+        int threshold = 70;
+        if (degrees >= (360 - threshold) || degrees < (threshold)) {
+            return Surface.ROTATION_0;
+        } else if (degrees < (90 + threshold)) {
+            return Surface.ROTATION_270;
+        } else if (degrees < 180 + threshold) {
+            return Surface.ROTATION_180;
+        } else {
+            return Surface.ROTATION_90;
+        }
+    }
+
+    /**
+     * @return how many factors {@param newRotation} is rotated 90 degrees clockwise.
+     * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise...
+     * A value of 0 means no rotation has been applied
+     */
+    public static int deltaRotation(int oldRotation, int newRotation) {
+        int delta = newRotation - oldRotation;
+        if (delta < 0) delta += 4;
+        return delta;
+    }
+
+    /**
+     * Creates a matrix to transform the given motion event specified by degrees.
+     * If {@param inverse} is {@code true}, the inverse of that matrix will be applied
+     */
+    public static void transformEvent(int degrees, MotionEvent ev, boolean inverse) {
+        Matrix transform = new Matrix();
+        transform.setRotate(degrees);
+        if (inverse) {
+            Matrix inv = new Matrix();
+            transform.invert(inv);
+            ev.transform(inv);
+        } else {
+            ev.transform(transform);
+        }
+        // TODO: Add scaling back in based on degrees
+//        if (getWidth() > 0 && getHeight() > 0) {
+//            float scale = ((float) getWidth()) / getHeight();
+//            transform.postScale(scale, 1 / scale);
+//        }
+    }
+
+    /**
+     * TODO(b/149658423): Have {@link com.android.quickstep.OrientationTouchTransformer
+     *   also use this}
+     */
+    public static Matrix getRotationMatrix(int screenWidth, int screenHeight, int displayRotation) {
+        Matrix m = new Matrix();
+        switch (displayRotation) {
+            case Surface.ROTATION_0:
+                return m;
+            case Surface.ROTATION_90:
+                m.setRotate(360 - RotationHelper.getDegreesFromRotation(displayRotation));
+                m.postTranslate(0, screenWidth);
+                break;
+            case Surface.ROTATION_270:
+                m.setRotate(360 - RotationHelper.getDegreesFromRotation(displayRotation));
+                m.postTranslate(screenHeight, 0);
+                break;
+        }
+        return m;
+    }
+
+    public static void mapRectFromNormalOrientation(RectF src, int screenWidth, int screenHeight,
+        int displayRotation) {
+        Matrix m = RotationHelper.getRotationMatrix(screenWidth, screenHeight, displayRotation);
+        m.mapRect(src);
+    }
+
+    public static void mapInverseRectFromNormalOrientation(RectF src, int screenWidth,
+        int screenHeight, int displayRotation) {
+        Matrix m = RotationHelper.getRotationMatrix(screenWidth, screenHeight, displayRotation);
+        Matrix inverse = new Matrix();
+        m.invert(inverse);
+        inverse.mapRect(src);
+    }
+
+    public static void getTargetRectForRotation(Rect srcOut, int screenWidth, int screenHeight,
+        int displayRotation) {
+        RectF wrapped = new RectF(srcOut);
+        Matrix m = RotationHelper.getRotationMatrix(screenWidth, screenHeight, displayRotation);
+        m.mapRect(wrapped);
+        wrapped.round(srcOut);
+    }
+
+    public static boolean isRotationLandscape(int rotation) {
+        return rotation == Surface.ROTATION_270 || rotation == Surface.ROTATION_90;
+    }
+
     @Override
     public String toString() {
         return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d,"
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 9df6241..34d69e9 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -230,7 +230,7 @@
     }
 
     @Override
-    public void onDragStart(boolean start) {
+    public void onDragStart(boolean start, float startDisplacement) {
         mStartState = mLauncher.getStateManager().getState();
         mIsLogContainerSet = false;
         if (mCurrentAnimation == null) {
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 30283da..1276ece 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -236,7 +236,7 @@
         } else {
             mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
             mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
-        }
+        } 
     }
 
     protected abstract boolean shouldScrollStart(PointF displacement);
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
new file mode 100644
index 0000000..3090d97
--- /dev/null
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.touch;
+
+import android.content.res.Resources;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.OverScroller;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+
+public class LandscapePagedViewHandler implements PagedOrientationHandler {
+    @Override
+    public float getCurrentAppAnimationScale(RectF src, RectF target) {
+        return src.height() / target.height();
+    }
+
+    @Override
+    public int getPrimaryValue(int x, int y) {
+        return y;
+    }
+
+    @Override
+    public int getSecondaryValue(int x, int y) {
+        return x;
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int secondaryScroll, int minMaxScroll) {
+        pagedView.superScrollTo(secondaryScroll, minMaxScroll);
+    }
+
+    @Override
+    public void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y) {
+        pagedView.scrollTo(pagedView.getScrollX() + x, unboundedScroll + y);
+    }
+
+    @Override
+    public void scrollerStartScroll(OverScroller scroller, int newPosition) {
+        scroller.startScroll(scroller.getCurrPos(), newPosition - scroller.getCurrPos());
+    }
+
+    @Override
+    public CurveProperties getCurveProperties(PagedView pagedView, Rect mInsets) {
+        int scroll = pagedView.getScrollY();
+        final int halfPageSize = pagedView.getNormalChildHeight() / 2;
+        final int screenCenter = mInsets.top + pagedView.getPaddingTop() + scroll + halfPageSize;
+        final int halfScreenSize = pagedView.getMeasuredHeight() / 2;
+        return new CurveProperties(scroll, halfPageSize, screenCenter, halfScreenSize);
+    }
+
+    @Override
+    public float getDragLengthFactor(int dimension, int transitionDragLength) {
+        return Math.min(1.0f, (float) dimension / transitionDragLength);
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement) {
+        return displacement > 0;
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int primaryScroll) {
+        pagedView.superScrollTo(pagedView.getScrollX(), primaryScroll);
+    }
+
+    @Override
+    public <T> void set(T target, Int2DAction<T> action, int param) {
+        action.call(target, 0, param);
+    }
+
+    @Override
+    public <T> void set(T target, Float2DAction<T> action, float param) {
+        action.call(target, 0, param);
+    }
+
+    @Override
+    public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
+        return event.getY(pointerIndex);
+    }
+
+    @Override
+    public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) {
+        return velocityTracker.getYVelocity(pointerId);
+    }
+
+    @Override
+    public int getMeasuredSize(View view) {
+        return view.getMeasuredHeight();
+    }
+
+    @Override
+    public int getPrimarySize(Rect rect) {
+        return rect.height();
+    }
+
+    @Override
+    public float getPrimarySize(RectF rect) {
+        return rect.height();
+    }
+
+    @Override
+    public int getSecondaryDimension(View view) {
+        return view.getWidth();
+    }
+
+    @Override
+    public ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
+        float offscreenTranslationY = dp.heightPx - view.getPaddingTop();
+        return new ScaleAndTranslation(1f, 0f, offscreenTranslationY);
+    }
+
+    @Override
+    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
+        return scaleAndTranslation.translationY;
+    }
+
+    @Override
+    public FloatProperty<View> getPrimaryViewTranslate() {
+        return VIEW_TRANSLATE_Y;
+    }
+
+    @Override
+    public FloatProperty<View> getSecondaryViewTranslate() {
+        return VIEW_TRANSLATE_X;
+    }
+
+    @Override
+    public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
+        view.setTranslationX(0);
+        view.setTranslationY(translation);
+    }
+
+    @Override
+    public float getViewCenterPosition(View view) {
+        return view.getTop() + view.getTranslationY();
+    }
+
+    @Override
+    public int getPrimaryScroll(View view) {
+        return view.getScrollY();
+    }
+
+    @Override
+    public float getPrimaryScale(View view) {
+        return view.getScaleY();
+    }
+
+    @Override
+    public void setMaxScroll(AccessibilityEvent event, int maxScroll) {
+        event.setMaxScrollY(maxScroll);
+    }
+
+    @Override
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return !Utilities.isRtl(resources);
+    }
+
+    @Override
+    public float getDegreesRotated() {
+        return 90;
+    }
+
+    @Override
+    public void offsetTaskRect(RectF rect, float value, int delta) {
+        if (delta == 0) {
+            rect.offset(value, 0);
+        } else if (delta == 1) {
+            rect.offset(0, -value);
+        } else if (delta == 2) {
+            rect.offset(-value, 0);
+        } else {
+            rect.offset(0, value);
+        }
+    }
+
+    @Override
+    public void mapRectFromNormalOrientation(Rect src, int screenWidth, int screenHeight) {
+        Matrix m = new Matrix();
+        m.setRotate(270);
+        m.postTranslate(0, screenWidth);
+        RectF newTarget = new RectF();
+        RectF oldTarget = new RectF(src);
+        m.mapRect(newTarget, oldTarget);
+        src.set((int)newTarget.left, (int)newTarget.top, (int)newTarget.right, (int)newTarget.bottom);
+    }
+
+    @Override
+    public int getChildStart(View view) {
+        return view.getTop();
+    }
+
+    @Override
+    public int getCenterForPage(View view, Rect insets) {
+        return (view.getPaddingLeft() + view.getMeasuredWidth() + insets.left
+            - insets.right - view.getPaddingRight()) / 2;
+    }
+
+    @Override
+    public int getScrollOffsetStart(View view, Rect insets) {
+        return insets.top + view.getPaddingTop();
+    }
+
+    @Override
+    public int getScrollOffsetEnd(View view, Rect insets) {
+        return view.getHeight() - view.getPaddingBottom() - insets.bottom;
+    }
+
+    @Override
+    public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
+        return HORIZONTAL;
+    }
+
+    @Override
+    public int getShortEdgeLength(DeviceProfile dp) {
+        return dp.heightPx;
+    }
+
+    @Override
+    public int getTaskDismissDirectionFactor() {
+        return 1;
+    }
+
+    @Override
+    public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
+        boolean layoutChild) {
+        final int childHeight = child.getMeasuredHeight();
+        final int childBottom = childStart + childHeight;
+        final int childWidth = child.getMeasuredWidth();
+        final int childLeft = pageCenter - childWidth/ 2;
+        if (layoutChild) {
+            child.layout(childLeft, childStart, childLeft + childWidth, childBottom);
+        }
+        return new ChildBounds(childHeight, childWidth, childBottom, childLeft);
+    }
+}
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
new file mode 100644
index 0000000..2f02076
--- /dev/null
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+package com.android.launcher3.touch;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.util.OverScroller;
+
+/**
+ * Abstraction layer to separate horizontal and vertical specific implementations
+ * for {@link com.android.launcher3.PagedView}. Majority of these implementations are (should be) as
+ * simple as choosing the correct X and Y analogous methods.
+ */
+public interface PagedOrientationHandler {
+
+    interface Int2DAction<T> {
+        void call(T target, int x, int y);
+    }
+    interface Float2DAction<T> {
+        void call(T target, float x, float y);
+    }
+    Int2DAction<View> VIEW_SCROLL_BY = View::scrollBy;
+    Int2DAction<View> VIEW_SCROLL_TO = View::scrollTo;
+    Float2DAction<Canvas> CANVAS_TRANSLATE = Canvas::translate;
+    <T> void set(T target, Int2DAction<T> action, int param);
+    <T> void set(T target, Float2DAction<T> action, float param);
+    float getPrimaryDirection(MotionEvent event, int pointerIndex);
+    float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
+    int getMeasuredSize(View view);
+    int getPrimarySize(Rect rect);
+    float getPrimarySize(RectF rect);
+    int getSecondaryDimension(View view);
+    LauncherState.ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view);
+    float getTranslationValue(LauncherState.ScaleAndTranslation scaleAndTranslation);
+    FloatProperty<View> getPrimaryViewTranslate();
+    FloatProperty<View> getSecondaryViewTranslate();
+    void setPrimaryAndResetSecondaryTranslate(View view, float translation);
+    float getViewCenterPosition(View view);
+    int getPrimaryScroll(View view);
+    float getPrimaryScale(View view);
+    int getChildStart(View view);
+    int getCenterForPage(View view, Rect insets);
+    int getScrollOffsetStart(View view, Rect insets);
+    int getScrollOffsetEnd(View view, Rect insets);
+    SingleAxisSwipeDetector.Direction getOppositeSwipeDirection();
+    int getShortEdgeLength(DeviceProfile dp);
+    int getTaskDismissDirectionFactor();
+    ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
+    void setMaxScroll(AccessibilityEvent event, int maxScroll);
+    boolean getRecentsRtlSetting(Resources resources);
+    float getDegreesRotated();
+    void offsetTaskRect(RectF rect, float value, int delta);
+    void mapRectFromNormalOrientation(Rect src, int screenWidth, int screenHeight);
+    float getCurrentAppAnimationScale(RectF src, RectF target);
+    int getPrimaryValue(int x, int y);
+    int getSecondaryValue(int x, int y);
+    void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll);
+    /** Uses {@params pagedView}.getScroll[X|Y]() method for the secondary amount*/
+    void delegateScrollTo(PagedView pagedView, int primaryScroll);
+    void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y);
+    void scrollerStartScroll(OverScroller scroller, int newPosition);
+    CurveProperties getCurveProperties(PagedView pagedView, Rect insets);
+    float getDragLengthFactor(int dimension, int transitionDragLength);
+    boolean isGoingUp(float displacement);
+
+    class CurveProperties {
+        public final int scroll;
+        public final int halfPageSize;
+        public final int screenCenter;
+        public final int halfScreenSize;
+
+        public CurveProperties(int scroll, int halfPageSize, int screenCenter, int halfScreenSize) {
+            this.scroll = scroll;
+            this.halfPageSize = halfPageSize;
+            this.screenCenter = screenCenter;
+            this.halfScreenSize = halfScreenSize;
+        }
+    }
+
+    class ChildBounds {
+
+        public final int primaryDimension;
+        public final int secondaryDimension;
+        public final int childPrimaryEnd;
+        public final int childSecondaryEnd;
+
+        ChildBounds(int primaryDimension, int secondaryDimension, int childPrimaryEnd,
+            int childSecondaryEnd) {
+            this.primaryDimension = primaryDimension;
+            this.secondaryDimension = secondaryDimension;
+            this.childPrimaryEnd = childPrimaryEnd;
+            this.childSecondaryEnd = childSecondaryEnd;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
new file mode 100644
index 0000000..fbd80bb
--- /dev/null
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.touch;
+
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.OverScroller;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+
+public class PortraitPagedViewHandler implements PagedOrientationHandler {
+
+    @Override
+    public float getCurrentAppAnimationScale(RectF src, RectF target) {
+        return src.width() / target.width();
+    }
+
+    @Override
+    public int getPrimaryValue(int x, int y) {
+        return x;
+    }
+
+    @Override
+    public int getSecondaryValue(int x, int y) {
+        return y;
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll) {
+        pagedView.superScrollTo(primaryScroll, secondaryScroll);
+    }
+
+    @Override
+    public void delegateScrollBy(PagedView pagedView, int unboundedScroll, int x, int y) {
+        pagedView.scrollTo(unboundedScroll + x, pagedView.getScrollY() + y);
+    }
+
+    @Override
+    public void scrollerStartScroll(OverScroller scroller, int newPosition) {
+        scroller.startScroll(newPosition - scroller.getCurrPos(), scroller.getCurrPos());
+    }
+
+    @Override
+    public CurveProperties getCurveProperties(PagedView pagedView, Rect mInsets) {
+        int scroll = pagedView.getScrollX();
+        final int halfPageSize = pagedView.getNormalChildWidth() / 2;
+        final int screenCenter = mInsets.left + pagedView.getPaddingLeft() + scroll + halfPageSize;
+        final int halfScreenSize = pagedView.getMeasuredWidth() / 2;
+        return new CurveProperties(scroll, halfPageSize, screenCenter, halfScreenSize);
+    }
+
+    @Override
+    public float getDragLengthFactor(int dimension, int transitionDragLength) {
+        return (float) dimension / transitionDragLength;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement) {
+        return displacement < 0;
+    }
+
+    @Override
+    public void delegateScrollTo(PagedView pagedView, int primaryScroll) {
+        pagedView.superScrollTo(primaryScroll, pagedView.getScrollY());
+    }
+
+    @Override
+    public <T> void set(T target, Int2DAction<T> action, int param) {
+        action.call(target, param, 0);
+    }
+
+    @Override
+    public <T> void set(T target, Float2DAction<T> action, float param) {
+        action.call(target, param, 0);
+    }
+
+    @Override
+    public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
+        return event.getX(pointerIndex);
+    }
+
+    @Override
+    public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) {
+        return velocityTracker.getXVelocity(pointerId);
+    }
+
+    @Override
+    public int getMeasuredSize(View view) {
+        return view.getMeasuredWidth();
+    }
+
+    @Override
+    public int getPrimarySize(Rect rect) {
+        return rect.width();
+    }
+
+    @Override
+    public float getPrimarySize(RectF rect) {
+        return rect.width();
+    }
+
+    @Override
+    public int getSecondaryDimension(View view) {
+        return view.getHeight();
+    }
+
+    @Override
+    public ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
+        float offscreenTranslationX = dp.widthPx - view.getPaddingStart();
+        return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
+    }
+
+    @Override
+    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
+        return scaleAndTranslation.translationX;
+    }
+
+    @Override
+    public FloatProperty<View> getPrimaryViewTranslate() {
+        return VIEW_TRANSLATE_X;
+    }
+
+    @Override
+    public FloatProperty<View> getSecondaryViewTranslate() {
+        return VIEW_TRANSLATE_Y;
+    }
+
+    @Override
+    public void setPrimaryAndResetSecondaryTranslate(View view, float translation) {
+        view.setTranslationX(translation);
+        view.setTranslationY(0);
+    }
+
+    @Override
+    public float getViewCenterPosition(View view) {
+        return view.getLeft() + view.getTranslationX();
+    }
+
+    @Override
+    public int getPrimaryScroll(View view) {
+        return view.getScrollX();
+    }
+
+    @Override
+    public float getPrimaryScale(View view) {
+        return view.getScaleX();
+    }
+
+    @Override
+    public void setMaxScroll(AccessibilityEvent event, int maxScroll) {
+        event.setMaxScrollX(maxScroll);
+    }
+
+    @Override
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return !Utilities.isRtl(resources);
+    }
+
+    @Override
+    public float getDegreesRotated() {
+        return 0;
+    }
+
+    @Override
+    public void offsetTaskRect(RectF rect, float value, int delta) {
+        if (delta == 0) {
+            rect.offset(value, 0);
+        } else if (delta == 1) {
+            rect.offset(0, -value);
+        } else if (delta == 2) {
+            rect.offset(-value, 0);
+        } else {
+            rect.offset(0, value);
+        }
+    }
+
+    @Override
+    public void mapRectFromNormalOrientation(Rect src, int screenWidth, int screenHeight) {
+        //no-op
+    }
+
+    @Override
+    public int getChildStart(View view) {
+        return view.getLeft();
+    }
+
+    @Override
+    public int getCenterForPage(View view, Rect insets) {
+        return (view.getPaddingTop() + view.getMeasuredHeight() + insets.top
+            - insets.bottom - view.getPaddingBottom()) / 2;
+    }
+
+    @Override
+    public int getScrollOffsetStart(View view, Rect insets) {
+        return insets.left + view.getPaddingLeft();
+    }
+
+    @Override
+    public int getScrollOffsetEnd(View view, Rect insets) {
+        return view.getWidth() - view.getPaddingRight() - insets.right;
+    }
+
+    @Override
+    public SingleAxisSwipeDetector.Direction getOppositeSwipeDirection() {
+        return VERTICAL;
+    }
+
+    @Override
+    public int getShortEdgeLength(DeviceProfile dp) {
+        return dp.widthPx;
+    }
+
+    @Override
+    public int getTaskDismissDirectionFactor() {
+        return -1;
+    }
+
+    @Override
+    public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
+        boolean layoutChild) {
+        final int childWidth = child.getMeasuredWidth();
+        final int childRight = childStart + childWidth;
+        final int childHeight = child.getMeasuredHeight();
+        final int childTop = pageCenter - childHeight / 2;
+        if (layoutChild) {
+            child.layout(childStart, childTop, childRight, childTop + childHeight);
+        }
+        return new ChildBounds(childWidth, childHeight, childRight, childTop);
+    }
+}
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
new file mode 100644
index 0000000..f1875cc
--- /dev/null
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.touch;
+
+import android.content.res.Resources;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.launcher3.Utilities;
+
+public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
+
+    @Override
+    public int getTaskDismissDirectionFactor() {
+        return -1;
+    }
+
+    @Override
+    public boolean getRecentsRtlSetting(Resources resources) {
+        return Utilities.isRtl(resources);
+    }
+
+    @Override
+    public void offsetTaskRect(RectF rect, float value, int delta) {
+        if (delta == 0) {
+            rect.offset(-value, 0);
+        } else if (delta == 1) {
+            rect.offset(0, value);
+        } else if (delta == 2) {
+            rect.offset(-value, 0);
+        } else {
+            rect.offset(0, -value);
+        }
+    }
+
+    @Override
+    public void mapRectFromNormalOrientation(Rect src, int screenWidth, int screenHeight) {
+        Matrix m = new Matrix();
+        m.setRotate(90);
+        m.postTranslate(screenHeight, 0);
+        RectF newTarget = new RectF();
+        RectF oldTarget = new RectF(src);
+        m.mapRect(newTarget, oldTarget);
+        src.set((int)newTarget.left, (int)newTarget.top, (int)newTarget.right, (int)newTarget.bottom);
+    }
+
+    @Override
+    public float getDegreesRotated() {
+        return 270;
+    }
+
+    @Override
+    public boolean isGoingUp(float displacement) {
+        return displacement < 0;
+    }
+}
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index 9d406f3..d725486 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -148,7 +148,8 @@
 
     @Override
     protected void reportDragStartInternal(boolean recatch) {
-        mListener.onDragStart(!recatch);
+        float startDisplacement = mDir.extractDirection(mSubtractDisplacement);
+        mListener.onDragStart(!recatch, startDisplacement);
     }
 
     @Override
@@ -165,8 +166,13 @@
 
     /** Listener to receive updates on the swipe. */
     public interface Listener {
-        /** @param start whether this was the original drag start, as opposed to a recatch. */
-        void onDragStart(boolean start);
+        /**
+         * TODO(b/150256055) consolidate all the different onDrag() methods into one
+         * @param start whether this was the original drag start, as opposed to a recatch.
+         * @param startDisplacement the initial touch displacement for the primary direction as
+         *        given by by {@link Direction#extractDirection(PointF)}
+         */
+        void onDragStart(boolean start, float startDisplacement);
 
         boolean onDrag(float displacement);
 
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index bdba39c..11c1029 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -149,8 +149,7 @@
     /* SingleAxisSwipeDetector.Listener */
 
     @Override
-    public void onDragStart(boolean start) {
-    }
+    public void onDragStart(boolean start, float startDisplacement) { }
 
     @Override
     public boolean onDrag(float displacement) {
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index b0ece77..472e1a1 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -89,7 +89,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 - mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -99,7 +99,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -107,7 +107,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener, never()).onDragStart(anyBoolean());
+        verify(mMockListener, never()).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -118,7 +118,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -129,7 +129,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -140,7 +140,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test
@@ -151,7 +151,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragStart(anyBoolean());
+        verify(mMockListener).onDragStart(anyBoolean(), anyFloat());
     }
 
     @Test