Converting orientation handler classes to kotlin

This conversion is a first step for the alignment bug fix and cleanup of the fake orientation classes. The goal is to have these classes following the new architecture using Kotlin, smaller, and testable.

Bug: 320314835
Bug: 328028913
Flag: N/A
Test: presubmit. manual.
Change-Id: If0d12ff8c69eca5528fbb6b092bddefd20bc37ba
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index e6a115a..6c1d4b1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -133,7 +133,7 @@
         // Create transition animations to split select
         RecentsPagedOrientationHandler orientationHandler =
                 ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
-        Pair<FloatProperty, FloatProperty> taskViewsFloat =
+        Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
                 orientationHandler.getSplitSelectTaskOffset(
                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
                         mLauncher.getDeviceProfile());
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 3e731e5..69eaf6a 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -49,6 +49,7 @@
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.views.ClearAllButton;
+import com.android.quickstep.views.RecentsView;
 
 /**
  * State controller for fallback recents activity
@@ -125,7 +126,7 @@
             setter.add(pa.buildAnim());
         }
 
-        Pair<FloatProperty, FloatProperty> taskViewsFloat =
+        Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
                 mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset(
                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
                         mActivity.getDeviceProfile());
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
deleted file mode 100644
index f345aeb..0000000
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
+++ /dev/null
@@ -1,704 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.orientation;
-
-import static android.view.Gravity.BOTTOM;
-import static android.view.Gravity.CENTER_VERTICAL;
-import static android.view.Gravity.END;
-import static android.view.Gravity.LEFT;
-import static android.view.Gravity.START;
-import static android.view.Gravity.TOP;
-import static android.view.View.LAYOUT_DIRECTION_RTL;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
-import static com.android.launcher3.Flags.enableOverviewIconMenu;
-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;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
-
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.ShapeDrawable;
-import android.util.FloatProperty;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.Surface;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.util.SplitConfigurationOptions;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
-import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.views.IconAppChipView;
-
-import java.util.Collections;
-import java.util.List;
-
-public class LandscapePagedViewHandler implements RecentsPagedOrientationHandler {
-
-    @Override
-    public <T> T getPrimaryValue(T x, T y) {
-        return y;
-    }
-
-    @Override
-    public <T> T getSecondaryValue(T x, T y) {
-        return x;
-    }
-
-    @Override
-    public int getPrimaryValue(int x, int y) {
-        return y;
-    }
-
-    @Override
-    public int getSecondaryValue(int x, int y) {
-        return x;
-    }
-
-    @Override
-    public float getPrimaryValue(float x, float y) {
-        return y;
-    }
-
-    @Override
-    public float getSecondaryValue(float x, float y) {
-        return x;
-    }
-
-    @Override
-    public boolean isLayoutNaturalToLauncher() {
-        return false;
-    }
-
-    @Override
-    public void adjustFloatingIconStartVelocity(PointF velocity) {
-        float oldX = velocity.x;
-        float oldY = velocity.y;
-        velocity.set(-oldY, oldX);
-    }
-
-    @Override
-    public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) {
-        // We don't need to check the "top" value here because the startRect is in the orientation
-        // of the app, not of the fixed portrait launcher.
-        if (outStartRect.left > deviceProfile.heightPx) {
-            outStartRect.offsetTo(0, outStartRect.top);
-        } else if (outStartRect.left < -deviceProfile.heightPx) {
-            outStartRect.offsetTo(0, outStartRect.top);
-        }
-    }
-
-    @Override
-    public <T> void setPrimary(T target, Int2DAction<T> action, int param) {
-        action.call(target, 0, param);
-    }
-
-    @Override
-    public <T> void setPrimary(T target, Float2DAction<T> action, float param) {
-        action.call(target, 0, param);
-    }
-
-    @Override
-    public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
-        action.call(target, param, 0);
-    }
-
-    @Override
-    public <T> void set(T target, Int2DAction<T> action, int primaryParam,
-            int secondaryParam) {
-        action.call(target, secondaryParam, primaryParam);
-    }
-
-    @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(View view) {
-        return view.getHeight();
-    }
-
-    @Override
-    public float getPrimarySize(RectF rect) {
-        return rect.height();
-    }
-
-    @Override
-    public float getStart(RectF rect) {
-        return rect.top;
-    }
-
-    @Override
-    public float getEnd(RectF rect) {
-        return rect.bottom;
-    }
-
-    @Override
-    public int getClearAllSidePadding(View view, boolean isRtl) {
-        return (isRtl ? view.getPaddingBottom() : - view.getPaddingTop()) / 2;
-    }
-
-    @Override
-    public int getSecondaryDimension(View view) {
-        return view.getWidth();
-    }
-
-    @Override
-    public FloatProperty<View> getPrimaryViewTranslate() {
-        return VIEW_TRANSLATE_Y;
-    }
-
-    @Override
-    public FloatProperty<View> getSecondaryViewTranslate() {
-        return VIEW_TRANSLATE_X;
-    }
-
-    @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 int getRotation() {
-        return Surface.ROTATION_90;
-    }
-
-    @Override
-    public void setPrimaryScale(View view, float scale) {
-        view.setScaleY(scale);
-    }
-
-    @Override
-    public void setSecondaryScale(View view, float scale) {
-        view.setScaleX(scale);
-    }
-
-    @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;
-    }
-
-    public int getSecondaryTranslationDirectionFactor() {
-        return 1;
-    }
-
-    @Override
-    public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
-        if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
-            return -1;
-        } else {
-            return 1;
-        }
-    }
-
-    @Override
-    public float getTaskMenuX(float x, View thumbnailView,
-            DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
-        return thumbnailView.getMeasuredWidth() + x - taskInsetMargin;
-    }
-
-    @Override
-    public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin, View taskViewIcon) {
-        BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
-        int taskMenuWidth = lp.width;
-        if (stagePosition == STAGE_POSITION_UNDEFINED) {
-            return y + taskInsetMargin
-                    + (thumbnailView.getMeasuredHeight() - taskMenuWidth) / 2f;
-        } else {
-            return y + taskInsetMargin;
-        }
-    }
-
-    @Override
-    public int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
-            @StagePosition int stagePosition) {
-        if (enableOverviewIconMenu()) {
-            return thumbnailView.getResources().getDimensionPixelSize(
-                    R.dimen.task_thumbnail_icon_menu_expanded_width);
-        }
-        if (stagePosition == SplitConfigurationOptions.STAGE_POSITION_UNDEFINED) {
-            return thumbnailView.getMeasuredWidth();
-        } else {
-            return thumbnailView.getMeasuredHeight();
-        }
-    }
-
-    @Override
-    public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile,
-            float taskMenuX, float taskMenuY) {
-        return (int) (taskMenuX - taskInsetMargin);
-    }
-
-    @Override
-    public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
-            LinearLayout taskMenuLayout, int dividerSpacing,
-            ShapeDrawable dividerDrawable) {
-        taskMenuLayout.setOrientation(LinearLayout.VERTICAL);
-        dividerDrawable.setIntrinsicHeight(dividerSpacing);
-        taskMenuLayout.setDividerDrawable(dividerDrawable);
-    }
-
-    @Override
-    public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
-            LinearLayout viewGroup, DeviceProfile deviceProfile) {
-        // Phone fake landscape
-        viewGroup.setOrientation(LinearLayout.HORIZONTAL);
-        lp.width = MATCH_PARENT;
-        lp.height = WRAP_CONTENT;
-    }
-
-    @Override
-    public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
-            int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
-            View[] thumbnailViews, int desiredTaskId, View banner) {
-        boolean isRtl = banner.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        float translationX = 0;
-        float translationY = 0;
-        FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
-        banner.setPivotX(0);
-        banner.setPivotY(0);
-        banner.setRotation(getDegreesRotated());
-        translationX = banner.getHeight();
-        FrameLayout.LayoutParams snapshotParams =
-                (FrameLayout.LayoutParams) thumbnailViews[0]
-                        .getLayoutParams();
-        bannerParams.gravity = TOP | (isRtl ? END : START);
-        if (splitBounds == null) {
-            // Single, fullscreen case
-            bannerParams.width = taskViewHeight - snapshotParams.topMargin;
-            return new Pair<>(translationX, Integer.valueOf(snapshotParams.topMargin).floatValue());
-        }
-
-        // Set correct width
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].getMeasuredHeight();
-        } else {
-            bannerParams.width = thumbnailViews[1].getMeasuredHeight();
-        }
-
-        // Set translations
-        if (desiredTaskId == splitBounds.rightBottomTaskId) {
-            float topLeftTaskPlusDividerPercent = splitBounds.appsStackedVertically
-                    ? (splitBounds.topTaskPercent + splitBounds.dividerHeightPercent)
-                    : (splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent);
-            translationY = snapshotParams.topMargin
-                    + ((taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent);
-        }
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            translationY = snapshotParams.topMargin;
-        }
-        return new Pair<>(translationX, translationY);
-    }
-
-    /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
-
-    @Override
-    public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
-        return HORIZONTAL;
-    }
-
-    @Override
-    public int getUpDirection(boolean isRtl) {
-        return isRtl ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE
-                : SingleAxisSwipeDetector.DIRECTION_POSITIVE;
-    }
-
-    @Override
-    public boolean isGoingUp(float displacement, boolean isRtl) {
-        return isRtl ? displacement < 0 : displacement > 0;
-    }
-
-    @Override
-    public int getTaskDragDisplacementFactor(boolean isRtl) {
-        return isRtl ? 1 : -1;
-    }
-
-    /* -------------------- */
-
-    @Override
-    public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
-            boolean layoutChild) {
-        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);
-    }
-
-    @SuppressWarnings("SuspiciousNameCombination")
-    @Override
-    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
-        return rect.left;
-    }
-
-    @Override
-    public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
-        // Add "left" side of phone which is actually the top
-        return Collections.singletonList(new SplitPositionOption(
-                R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen,
-                STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
-    }
-
-    @Override
-    public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset,
-            DeviceProfile dp, @StagePosition int stagePosition, Rect out) {
-        // In fake land/seascape, the placeholder always needs to go to the "top" of the device,
-        // which is the same bounds as 0 rotation.
-        int width = dp.widthPx;
-        int insetSizeAdjustment = getPlaceholderSizeAdjustment(dp);
-        out.set(0, 0, width, placeholderHeight + insetSizeAdjustment);
-        out.inset(placeholderInset, 0);
-
-        // Adjust the top to account for content off screen. This will help to animate the view in
-        // with rounded corners.
-        int screenWidth = dp.widthPx;
-        int screenHeight = dp.heightPx;
-        int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset)
-                / screenWidth);
-        out.top -= (totalHeight - placeholderHeight);
-    }
-
-    @Override
-    public void updateSplitIconParams(View out, float onScreenRectCenterX,
-            float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY,
-            int drawableWidth, int drawableHeight, DeviceProfile dp,
-            @StagePosition int stagePosition) {
-        float insetAdjustment = getPlaceholderSizeAdjustment(dp) / 2f;
-        out.setX(onScreenRectCenterX / fullscreenScaleX
-                - 1.0f * drawableWidth / 2);
-        out.setY((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY
-                - 1.0f * drawableHeight / 2);
-    }
-
-    /**
-     * The split placeholder comes with a default inset to buffer the icon from the top of the
-     * screen. But if the device already has a large inset (from cutouts etc), use that instead.
-     */
-    private int getPlaceholderSizeAdjustment(DeviceProfile dp) {
-        return Math.max(dp.getInsets().top - dp.splitPlaceholderInset, 0);
-    }
-
-    @Override
-    public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
-            int splitInstructionsWidth) {
-        out.setPivotX(0);
-        out.setPivotY(splitInstructionsHeight);
-        out.setRotation(getDegreesRotated());
-        int distanceToEdge = out.getResources().getDimensionPixelSize(
-                R.dimen.split_instructions_bottom_margin_phone_landscape);
-        // Adjust for any insets on the left edge
-        int insetCorrectionX = dp.getInsets().left;
-        // Center the view in case of unbalanced insets on top or bottom of screen
-        int insetCorrectionY = (dp.getInsets().bottom - dp.getInsets().top) / 2;
-        out.setTranslationX(distanceToEdge - insetCorrectionX);
-        out.setTranslationY(((-splitInstructionsHeight - splitInstructionsWidth) / 2f)
-                + insetCorrectionY);
-        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
-        // Setting gravity to LEFT instead of the lint-recommended START because we always want this
-        // view to be screen-left when phone is in landscape, regardless of the RtL setting.
-        lp.gravity = LEFT | CENTER_VERTICAL;
-        out.setLayoutParams(lp);
-    }
-
-    @Override
-    public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
-            @StagePosition int stagePosition, Rect out1, Rect out2) {
-        // In fake land/seascape, the window bounds are always top and bottom half
-        int screenHeight = dp.heightPx;
-        int screenWidth = dp.widthPx;
-        out1.set(0, 0, screenWidth, screenHeight / 2  - splitDividerSize);
-        out2.set(0, screenHeight / 2  + splitDividerSize, screenWidth, screenHeight);
-    }
-
-    @Override
-    public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect,
-            SplitBounds splitInfo, int desiredStagePosition) {
-        float topLeftTaskPercent = splitInfo.appsStackedVertically
-                ? splitInfo.topTaskPercent
-                : splitInfo.leftTaskPercent;
-        float dividerBarPercent = splitInfo.appsStackedVertically
-                ? splitInfo.dividerHeightPercent
-                : splitInfo.dividerWidthPercent;
-
-        if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) {
-            outRect.bottom = outRect.top + (int) (outRect.height() * topLeftTaskPercent);
-        } else {
-            outRect.top += (int) (outRect.height() * (topLeftTaskPercent + dividerBarPercent));
-        }
-    }
-
-    @Override
-    public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
-            int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp,
-            boolean isRtl) {
-        FrameLayout.LayoutParams primaryParams =
-                (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
-        FrameLayout.LayoutParams secondaryParams =
-                (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
-
-        // Swap the margins that are set in TaskView#setRecentsOrientedState()
-        secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx;
-        primaryParams.topMargin = 0;
-
-        // Measure and layout the thumbnails bottom up, since the primary is on the visual left
-        // (portrait bottom) and secondary is on the right (portrait top)
-        int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
-        int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
-        int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.dividerHeightPercent
-                : splitBoundsConfig.dividerWidthPercent));
-
-        Pair<Point, Point> taskViewSizes =
-                getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
-
-        int translationY = taskViewSizes.first.y + spaceAboveSnapshot + dividerBar;
-        primarySnapshot.setTranslationY(spaceAboveSnapshot);
-        secondarySnapshot.setTranslationY(translationY - spaceAboveSnapshot);
-
-        primarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)
-        );
-        secondarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY)
-        );
-    }
-
-    @Override
-    public Pair<Point, Point> getGroupedTaskViewSizes(
-            DeviceProfile dp,
-            SplitBounds splitBoundsConfig,
-            int parentWidth,
-            int parentHeight) {
-        int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
-        int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
-        int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.dividerHeightPercent
-                : splitBoundsConfig.dividerWidthPercent));
-        float taskPercent = splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.topTaskPercent
-                : splitBoundsConfig.leftTaskPercent;
-
-        Point firstTaskViewSize = new Point(
-                parentWidth,
-                (int) (totalThumbnailHeight * taskPercent)
-        );
-        Point secondTaskViewSize = new Point(
-                parentWidth,
-                totalThumbnailHeight - firstTaskViewSize.y - dividerBar
-        );
-
-        return new Pair<>(firstTaskViewSize, secondTaskViewSize);
-    }
-
-    @Override
-    public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
-            int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
-        iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
-        iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2;
-        iconParams.leftMargin = 0;
-        iconParams.topMargin = thumbnailTopMargin / 2;
-        iconParams.bottomMargin = 0;
-    }
-
-    @Override
-    public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
-            int chipChildMarginStart) {
-        iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
-        iconParams.setMarginStart(chipChildMarginStart);
-        iconParams.topMargin = 0;
-    }
-
-    @Override
-    public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
-            FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
-        boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        iconMenuParams.gravity = (isRtl ? START : END) | (isRtl ? BOTTOM : TOP);
-        iconMenuParams.setMarginStart(isRtl ? iconMenuMargin : 0);
-        iconMenuParams.topMargin = iconMenuMargin;
-        iconMenuParams.bottomMargin = isRtl ? iconMenuMargin : 0;
-        iconMenuParams.setMarginEnd(iconMenuMargin);
-
-        iconAppChipView.setPivotX(isRtl ? iconMenuParams.width - (iconMenuParams.height / 2f)
-                : iconMenuParams.width / 2f);
-        iconAppChipView.setPivotY(
-                isRtl ? (iconMenuParams.height / 2f) : iconMenuParams.width / 2f);
-        iconAppChipView.setSplitTranslationY(0);
-        iconAppChipView.setRotation(getDegreesRotated());
-    }
-
-    @Override
-    public void setSplitIconParams(View primaryIconView, View secondaryIconView,
-            int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
-            int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
-            DeviceProfile deviceProfile, SplitBounds splitConfig) {
-        FrameLayout.LayoutParams primaryIconParams =
-                (FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
-        FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu()
-                ? (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams()
-                : new FrameLayout.LayoutParams(primaryIconParams);
-
-        // We calculate the "midpoint" of the thumbnail area, and place the icons there.
-        // This is the place where the thumbnail area splits by default, in a near-50/50 split.
-        // It is usually not exactly 50/50, due to insets/screen cutouts.
-        int fullscreenInsetThickness = deviceProfile.getInsets().top
-                - deviceProfile.getInsets().bottom;
-        int fullscreenMidpointFromBottom = ((deviceProfile.heightPx - fullscreenInsetThickness)
-                / 2);
-        float midpointFromBottomPct = (float) fullscreenMidpointFromBottom / deviceProfile.heightPx;
-        float insetPct = (float) fullscreenInsetThickness / deviceProfile.heightPx;
-        int spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx;
-        int overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots;
-        int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct);
-        int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct);
-
-        if (enableOverviewIconMenu()) {
-            primaryIconParams.gravity = isRtl ? BOTTOM | START : TOP | END;
-            secondaryIconParams.gravity = isRtl ? BOTTOM | START : TOP | END;
-        } else {
-            primaryIconParams.gravity = BOTTOM | (isRtl ? START : END);
-            secondaryIconParams.gravity = BOTTOM | (isRtl ? START : END);
-        }
-        primaryIconView.setTranslationX(0);
-        secondaryIconView.setTranslationX(0);
-        if (enableOverviewIconMenu()) {
-            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
-            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
-            if (primaryIconView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
-                secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
-                primaryAppChipView.setSplitTranslationY(0);
-            } else {
-                int secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight;
-                primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight);
-            }
-        } else if (splitConfig.initiatedFromSeascape) {
-            // if the split was initiated from seascape,
-            // the task on the right (secondary) is slightly larger
-            primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset);
-            secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset
-                    + taskIconHeight);
-        } else {
-            // if not,
-            // the task on the left (primary) is slightly larger
-            primaryIconView.setTranslationY(-bottomToMidpointOffset);
-            secondaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight);
-        }
-
-        primaryIconView.setLayoutParams(primaryIconParams);
-        secondaryIconView.setLayoutParams(secondaryIconParams);
-    }
-
-    @Override
-    public int getDefaultSplitPosition(DeviceProfile deviceProfile) {
-        throw new IllegalStateException("Default position not available in fake landscape");
-    }
-
-    @Override
-    public Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
-            FloatProperty secondary, DeviceProfile deviceProfile) {
-        return new Pair<>(primary, secondary);
-    }
-
-    @Override
-    public float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect,
-            @StagePosition int stagePosition, DeviceProfile dp) {
-        float currentTranslationY = floatingTask.getTranslationY();
-        return currentTranslationY - onScreenRect.height();
-    }
-
-    @Override
-    public void setFloatingTaskPrimaryTranslation(View floatingTask, float translation,
-            DeviceProfile dp) {
-        floatingTask.setTranslationY(translation);
-    }
-
-    @Override
-    public Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) {
-        return floatingTask.getTranslationY();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
new file mode 100644
index 0000000..39fb158
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -0,0 +1,659 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.orientation
+
+import android.content.res.Resources
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.ShapeDrawable
+import android.util.FloatProperty
+import android.util.Pair
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.Surface
+import android.view.VelocityTracker
+import android.view.View
+import android.view.View.MeasureSpec
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.core.util.component1
+import androidx.core.util.component2
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherAnimUtils
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds
+import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction
+import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.SplitConfigurationOptions.*
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
+import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.views.IconAppChipView
+import kotlin.math.max
+
+open class LandscapePagedViewHandler : RecentsPagedOrientationHandler {
+    override fun <T> getPrimaryValue(x: T, y: T): T = y
+
+    override fun <T> getSecondaryValue(x: T, y: T): T = x
+
+    override fun getPrimaryValue(x: Int, y: Int): Int = y
+
+    override fun getSecondaryValue(x: Int, y: Int): Int = x
+
+    override fun getPrimaryValue(x: Float, y: Float): Float = y
+
+    override fun getSecondaryValue(x: Float, y: Float): Float = x
+
+    override val isLayoutNaturalToLauncher: Boolean = false
+
+    override fun adjustFloatingIconStartVelocity(velocity: PointF) {
+        val oldX = velocity.x
+        val oldY = velocity.y
+        velocity.set(-oldY, oldX)
+    }
+
+    override fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile) {
+        // We don't need to check the "top" value here because the startRect is in the orientation
+        // of the app, not of the fixed portrait launcher.
+        if (outStartRect.left > deviceProfile.heightPx) {
+            outStartRect.offsetTo(0f, outStartRect.top)
+        } else if (outStartRect.left < -deviceProfile.heightPx) {
+            outStartRect.offsetTo(0f, outStartRect.top)
+        }
+    }
+
+    override fun <T> setPrimary(target: T, action: Int2DAction<T>, param: Int) =
+        action.call(target, 0, param)
+
+    override fun <T> setPrimary(target: T, action: Float2DAction<T>, param: Float) =
+        action.call(target, 0f, param)
+
+    override fun <T> setSecondary(target: T, action: Float2DAction<T>, param: Float) =
+        action.call(target, param, 0f)
+
+    override fun <T> set(
+        target: T,
+        action: Int2DAction<T>,
+        primaryParam: Int,
+        secondaryParam: Int
+    ) = action.call(target, secondaryParam, primaryParam)
+
+    override fun getPrimaryDirection(event: MotionEvent, pointerIndex: Int): Float =
+        event.getY(pointerIndex)
+
+    override fun getPrimaryVelocity(velocityTracker: VelocityTracker, pointerId: Int): Float =
+        velocityTracker.getYVelocity(pointerId)
+
+    override fun getMeasuredSize(view: View): Int = view.measuredHeight
+
+    override fun getPrimarySize(view: View): Int = view.height
+
+    override fun getPrimarySize(rect: RectF): Float = rect.height()
+
+    override fun getStart(rect: RectF): Float = rect.top
+
+    override fun getEnd(rect: RectF): Float = rect.bottom
+
+    override fun getClearAllSidePadding(view: View, isRtl: Boolean): Int =
+        if (isRtl) view.paddingBottom / 2 else -view.paddingTop / 2
+
+    override fun getSecondaryDimension(view: View): Int = view.width
+
+    override val primaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_Y
+
+    override val secondaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_X
+
+    override fun getPrimaryScroll(view: View): Int = view.scrollY
+
+    override fun getPrimaryScale(view: View): Float = view.scaleY
+
+    override fun setMaxScroll(event: AccessibilityEvent, maxScroll: Int) {
+        event.maxScrollY = maxScroll
+    }
+
+    override fun getRecentsRtlSetting(resources: Resources): Boolean = !Utilities.isRtl(resources)
+
+    override val degreesRotated: Float = 90f
+
+    override val rotation: Int = Surface.ROTATION_90
+
+    override fun setPrimaryScale(view: View, scale: Float) {
+        view.scaleY = scale
+    }
+
+    override fun setSecondaryScale(view: View, scale: Float) {
+        view.scaleX = scale
+    }
+
+    override fun getChildStart(view: View): Int = view.top
+
+    override fun getCenterForPage(view: View, insets: Rect): Int =
+        (view.paddingLeft + view.measuredWidth + insets.left - insets.right - view.paddingRight) / 2
+
+    override fun getScrollOffsetStart(view: View, insets: Rect): Int = insets.top + view.paddingTop
+
+    override fun getScrollOffsetEnd(view: View, insets: Rect): Int =
+        view.height - view.paddingBottom - insets.bottom
+
+    override val secondaryTranslationDirectionFactor: Int = 1
+
+    override fun getSplitTranslationDirectionFactor(
+        stagePosition: Int,
+        deviceProfile: DeviceProfile
+    ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1
+
+    override fun getTaskMenuX(
+        x: Float,
+        thumbnailView: View,
+        deviceProfile: DeviceProfile,
+        taskInsetMargin: Float,
+        taskViewIcon: View
+    ): Float = thumbnailView.measuredWidth + x - taskInsetMargin
+
+    override fun getTaskMenuY(
+        y: Float,
+        thumbnailView: View,
+        stagePosition: Int,
+        taskMenuView: View,
+        taskInsetMargin: Float,
+        taskViewIcon: View
+    ): Float {
+        val layoutParams = taskMenuView.layoutParams as BaseDragLayer.LayoutParams
+        var taskMenuY = y + taskInsetMargin
+
+        if (stagePosition == STAGE_POSITION_UNDEFINED) {
+            taskMenuY += (thumbnailView.measuredHeight - layoutParams.width) / 2f
+        }
+
+        return taskMenuY
+    }
+
+    override fun getTaskMenuWidth(
+        thumbnailView: View,
+        deviceProfile: DeviceProfile,
+        @StagePosition stagePosition: Int
+    ): Int =
+        when {
+            Flags.enableOverviewIconMenu() ->
+                thumbnailView.resources.getDimensionPixelSize(
+                    R.dimen.task_thumbnail_icon_menu_expanded_width
+                )
+            stagePosition == STAGE_POSITION_UNDEFINED -> thumbnailView.measuredWidth
+            else -> thumbnailView.measuredHeight
+        }
+
+    override fun getTaskMenuHeight(
+        taskInsetMargin: Float,
+        deviceProfile: DeviceProfile,
+        taskMenuX: Float,
+        taskMenuY: Float
+    ): Int = (taskMenuX - taskInsetMargin).toInt()
+
+    override fun setTaskOptionsMenuLayoutOrientation(
+        deviceProfile: DeviceProfile,
+        taskMenuLayout: LinearLayout,
+        dividerSpacing: Int,
+        dividerDrawable: ShapeDrawable
+    ) {
+        taskMenuLayout.orientation = LinearLayout.VERTICAL
+        dividerDrawable.intrinsicHeight = dividerSpacing
+        taskMenuLayout.dividerDrawable = dividerDrawable
+    }
+
+    override fun setLayoutParamsForTaskMenuOptionItem(
+        lp: LinearLayout.LayoutParams,
+        viewGroup: LinearLayout,
+        deviceProfile: DeviceProfile
+    ) {
+        // Phone fake landscape
+        viewGroup.orientation = LinearLayout.HORIZONTAL
+        lp.width = ViewGroup.LayoutParams.MATCH_PARENT
+        lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
+    }
+
+    override fun getDwbLayoutTranslations(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        splitBounds: SplitBounds?,
+        deviceProfile: DeviceProfile,
+        thumbnailViews: Array<View>,
+        desiredTaskId: Int,
+        banner: View
+    ): Pair<Float, Float> {
+        val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
+        val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
+        val translationX = banner.height.toFloat()
+
+        val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
+        bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START
+        banner.pivotX = 0f
+        banner.pivotY = 0f
+        banner.rotation = degreesRotated
+
+        if (splitBounds == null) {
+            // Single, fullscreen case
+            bannerParams.width = taskViewHeight - snapshotParams.topMargin
+            return Pair(translationX, snapshotParams.topMargin.toFloat())
+        }
+
+        // Set correct width and translations
+        val translationY: Float
+        if (desiredTaskId == splitBounds.leftTopTaskId) {
+            bannerParams.width = thumbnailViews[0].measuredHeight
+            translationY = snapshotParams.topMargin.toFloat()
+        } else {
+            bannerParams.width = thumbnailViews[1].measuredHeight
+            val topLeftTaskPlusDividerPercent =
+                if (splitBounds.appsStackedVertically) {
+                    splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
+                } else {
+                    splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
+                }
+            translationY =
+                snapshotParams.topMargin +
+                    (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
+        }
+
+        return Pair(translationX, translationY)
+    }
+
+    /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+    override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction =
+        SingleAxisSwipeDetector.HORIZONTAL
+
+    override fun getUpDirection(isRtl: Boolean): Int =
+        if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+        else SingleAxisSwipeDetector.DIRECTION_POSITIVE
+
+    override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
+        if (isRtl) displacement < 0 else displacement > 0
+
+    override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1
+    /* -------------------- */
+
+    override fun getChildBounds(
+        child: View,
+        childStart: Int,
+        pageCenter: Int,
+        layoutChild: Boolean
+    ): ChildBounds {
+        val childHeight = child.measuredHeight
+        val childWidth = child.measuredWidth
+        val childBottom = childStart + childHeight
+        val childLeft = pageCenter - childWidth / 2
+        if (layoutChild) {
+            child.layout(childLeft, childStart, childLeft + childWidth, childBottom)
+        }
+        return ChildBounds(childHeight, childWidth, childBottom, childLeft)
+    }
+
+    override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int = rect.left
+
+    override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> =
+        // Add "left" side of phone which is actually the top
+        listOf(
+            SplitPositionOption(
+                R.drawable.ic_split_horizontal,
+                R.string.recent_task_option_split_screen,
+                STAGE_POSITION_TOP_OR_LEFT,
+                STAGE_TYPE_MAIN
+            )
+        )
+
+    override fun getInitialSplitPlaceholderBounds(
+        placeholderHeight: Int,
+        placeholderInset: Int,
+        dp: DeviceProfile,
+        @StagePosition stagePosition: Int,
+        out: Rect
+    ) {
+        // In fake land/seascape, the placeholder always needs to go to the "top" of the device,
+        // which is the same bounds as 0 rotation.
+        val width = dp.widthPx
+        val insetSizeAdjustment = getPlaceholderSizeAdjustment(dp)
+        out.set(0, 0, width, placeholderHeight + insetSizeAdjustment)
+        out.inset(placeholderInset, 0)
+
+        // Adjust the top to account for content off screen. This will help to animate the view in
+        // with rounded corners.
+        val screenWidth = dp.widthPx
+        val screenHeight = dp.heightPx
+        val totalHeight =
+            (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) / screenWidth).toInt()
+        out.top -= totalHeight - placeholderHeight
+    }
+
+    override fun updateSplitIconParams(
+        out: View,
+        onScreenRectCenterX: Float,
+        onScreenRectCenterY: Float,
+        fullscreenScaleX: Float,
+        fullscreenScaleY: Float,
+        drawableWidth: Int,
+        drawableHeight: Int,
+        dp: DeviceProfile,
+        @StagePosition stagePosition: Int
+    ) {
+        val insetAdjustment = getPlaceholderSizeAdjustment(dp) / 2f
+        out.x = (onScreenRectCenterX / fullscreenScaleX - 1.0f * drawableWidth / 2)
+        out.y =
+            ((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY - 1.0f * drawableHeight / 2)
+    }
+
+    /**
+     * The split placeholder comes with a default inset to buffer the icon from the top of the
+     * screen. But if the device already has a large inset (from cutouts etc), use that instead.
+     */
+    private fun getPlaceholderSizeAdjustment(dp: DeviceProfile?): Int =
+        max((dp!!.insets.top - dp.splitPlaceholderInset).toDouble(), 0.0).toInt()
+
+    override fun setSplitInstructionsParams(
+        out: View,
+        dp: DeviceProfile,
+        splitInstructionsHeight: Int,
+        splitInstructionsWidth: Int
+    ) {
+        out.pivotX = 0f
+        out.pivotY = splitInstructionsHeight.toFloat()
+        out.rotation = degreesRotated
+        val distanceToEdge =
+            out.resources.getDimensionPixelSize(
+                R.dimen.split_instructions_bottom_margin_phone_landscape
+            )
+        // Adjust for any insets on the left edge
+        val insetCorrectionX = dp.insets.left
+        // Center the view in case of unbalanced insets on top or bottom of screen
+        val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2
+        out.translationX = (distanceToEdge - insetCorrectionX).toFloat()
+        out.translationY =
+            (-splitInstructionsHeight - splitInstructionsWidth) / 2f + insetCorrectionY
+        // Setting gravity to LEFT instead of the lint-recommended START because we always want this
+        // view to be screen-left when phone is in landscape, regardless of the RtL setting.
+        val lp = out.layoutParams as FrameLayout.LayoutParams
+        lp.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
+        out.layoutParams = lp
+    }
+
+    override fun getFinalSplitPlaceholderBounds(
+        splitDividerSize: Int,
+        dp: DeviceProfile,
+        @StagePosition stagePosition: Int,
+        out1: Rect,
+        out2: Rect
+    ) {
+        // In fake land/seascape, the window bounds are always top and bottom half
+        val screenHeight = dp.heightPx
+        val screenWidth = dp.widthPx
+        out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize)
+        out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight)
+    }
+
+    override fun setSplitTaskSwipeRect(
+        dp: DeviceProfile,
+        outRect: Rect,
+        splitInfo: SplitBounds,
+        desiredStagePosition: Int
+    ) {
+        val topLeftTaskPercent: Float
+        val dividerBarPercent: Float
+        if (splitInfo.appsStackedVertically) {
+            topLeftTaskPercent = splitInfo.topTaskPercent
+            dividerBarPercent = splitInfo.dividerHeightPercent
+        } else {
+            topLeftTaskPercent = splitInfo.leftTaskPercent
+            dividerBarPercent = splitInfo.dividerWidthPercent
+        }
+
+        if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+            outRect.bottom = outRect.top + (outRect.height() * topLeftTaskPercent).toInt()
+        } else {
+            outRect.top += (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt()
+        }
+    }
+
+    override fun measureGroupedTaskViewThumbnailBounds(
+        primarySnapshot: View,
+        secondarySnapshot: View,
+        parentWidth: Int,
+        parentHeight: Int,
+        splitBoundsConfig: SplitBounds,
+        dp: DeviceProfile,
+        isRtl: Boolean
+    ) {
+        val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
+        val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
+
+        // Swap the margins that are set in TaskView#setRecentsOrientedState()
+        secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx
+        primaryParams.topMargin = 0
+
+        // Measure and layout the thumbnails bottom up, since the primary is on the visual left
+        // (portrait bottom) and secondary is on the right (portrait top)
+        val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
+        val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
+        val dividerBar =
+            Math.round(
+                totalThumbnailHeight *
+                    if (splitBoundsConfig.appsStackedVertically)
+                        splitBoundsConfig.dividerHeightPercent
+                    else splitBoundsConfig.dividerWidthPercent
+            )
+        val (taskViewFirst, taskViewSecond) =
+            getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
+
+        primarySnapshot.translationY = spaceAboveSnapshot.toFloat()
+        primarySnapshot.measure(
+            MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY),
+            MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY)
+        )
+        val translationY = taskViewFirst.y + spaceAboveSnapshot + dividerBar
+        secondarySnapshot.translationY = (translationY - spaceAboveSnapshot).toFloat()
+        secondarySnapshot.measure(
+            MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY),
+            MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY)
+        )
+    }
+
+    override fun getGroupedTaskViewSizes(
+        dp: DeviceProfile,
+        splitBoundsConfig: SplitBounds,
+        parentWidth: Int,
+        parentHeight: Int
+    ): Pair<Point, Point> {
+        val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
+        val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
+        val dividerBar =
+            Math.round(
+                totalThumbnailHeight *
+                    if (splitBoundsConfig.appsStackedVertically)
+                        splitBoundsConfig.dividerHeightPercent
+                    else splitBoundsConfig.dividerWidthPercent
+            )
+        val taskPercent =
+            if (splitBoundsConfig.appsStackedVertically) {
+                splitBoundsConfig.topTaskPercent
+            } else {
+                splitBoundsConfig.leftTaskPercent
+            }
+        val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
+        val secondTaskViewSize =
+            Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
+        return Pair(firstTaskViewSize, secondTaskViewSize)
+    }
+
+    override fun setTaskIconParams(
+        iconParams: FrameLayout.LayoutParams,
+        taskIconMargin: Int,
+        taskIconHeight: Int,
+        thumbnailTopMargin: Int,
+        isRtl: Boolean
+    ) {
+        iconParams.gravity =
+            if (isRtl) {
+                Gravity.START or Gravity.CENTER_VERTICAL
+            } else {
+                Gravity.END or Gravity.CENTER_VERTICAL
+            }
+        iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2
+        iconParams.leftMargin = 0
+        iconParams.topMargin = thumbnailTopMargin / 2
+        iconParams.bottomMargin = 0
+    }
+
+    override fun setIconAppChipChildrenParams(
+        iconParams: FrameLayout.LayoutParams,
+        chipChildMarginStart: Int
+    ) {
+        iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
+        iconParams.marginStart = chipChildMarginStart
+        iconParams.topMargin = 0
+    }
+
+    override fun setIconAppChipMenuParams(
+        iconAppChipView: IconAppChipView,
+        iconMenuParams: FrameLayout.LayoutParams,
+        iconMenuMargin: Int,
+        thumbnailTopMargin: Int
+    ) {
+        val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL
+
+        if (isRtl) {
+            iconMenuParams.gravity = Gravity.START or Gravity.BOTTOM
+            iconMenuParams.marginStart = iconMenuMargin
+            iconMenuParams.bottomMargin = iconMenuMargin
+            iconAppChipView.pivotX = iconMenuParams.width - iconMenuParams.height / 2f
+            iconAppChipView.pivotY = iconMenuParams.height / 2f
+        } else {
+            iconMenuParams.gravity = Gravity.END or Gravity.TOP
+            iconMenuParams.marginStart = 0
+            iconMenuParams.bottomMargin = 0
+            iconAppChipView.pivotX = iconMenuParams.width / 2f
+            iconAppChipView.pivotY = iconMenuParams.width / 2f
+        }
+
+        iconMenuParams.topMargin = iconMenuMargin
+        iconMenuParams.marginEnd = iconMenuMargin
+        iconAppChipView.setSplitTranslationY(0f)
+        iconAppChipView.setRotation(degreesRotated)
+    }
+
+    override fun setSplitIconParams(
+        primaryIconView: View,
+        secondaryIconView: View,
+        taskIconHeight: Int,
+        primarySnapshotWidth: Int,
+        primarySnapshotHeight: Int,
+        groupedTaskViewHeight: Int,
+        groupedTaskViewWidth: Int,
+        isRtl: Boolean,
+        deviceProfile: DeviceProfile,
+        splitConfig: SplitBounds
+    ) {
+        val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams
+        val secondaryIconParams =
+            if (Flags.enableOverviewIconMenu())
+                secondaryIconView.layoutParams as FrameLayout.LayoutParams
+            else FrameLayout.LayoutParams(primaryIconParams)
+
+        // We calculate the "midpoint" of the thumbnail area, and place the icons there.
+        // This is the place where the thumbnail area splits by default, in a near-50/50 split.
+        // It is usually not exactly 50/50, due to insets/screen cutouts.
+        val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom)
+        val fullscreenMidpointFromBottom = ((deviceProfile.heightPx - fullscreenInsetThickness) / 2)
+        val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx
+        val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx
+        val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx
+        val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots
+        val bottomToMidpointOffset =
+            (overviewThumbnailAreaThickness * midpointFromBottomPct).toInt()
+        val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt()
+        if (Flags.enableOverviewIconMenu()) {
+            val gravity = if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END
+            primaryIconParams.gravity = gravity
+            secondaryIconParams.gravity = gravity
+        } else {
+            primaryIconParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END
+            secondaryIconParams.gravity =
+                Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END
+        }
+        primaryIconView.translationX = 0f
+        secondaryIconView.translationX = 0f
+        when {
+            Flags.enableOverviewIconMenu() -> {
+                val primaryAppChipView = primaryIconView as IconAppChipView
+                val secondaryAppChipView = secondaryIconView as IconAppChipView
+                if (primaryIconView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+                    secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat())
+                    primaryAppChipView.setSplitTranslationY(0f)
+                } else {
+                    val secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight
+                    primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight.toFloat())
+                }
+            }
+            splitConfig.initiatedFromSeascape -> {
+                // if the split was initiated from seascape,
+                // the task on the right (secondary) is slightly larger
+                primaryIconView.translationY = (-bottomToMidpointOffset - insetOffset).toFloat()
+                secondaryIconView.translationY =
+                    (-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
+            }
+            else -> {
+                // if not,
+                // the task on the left (primary) is slightly larger
+                primaryIconView.translationY = -bottomToMidpointOffset.toFloat()
+                secondaryIconView.translationY =
+                    (-bottomToMidpointOffset + taskIconHeight).toFloat()
+            }
+        }
+        primaryIconView.layoutParams = primaryIconParams
+        secondaryIconView.layoutParams = secondaryIconParams
+    }
+
+    override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int {
+        throw IllegalStateException("Default position not available in fake landscape")
+    }
+
+    override fun <T> getSplitSelectTaskOffset(
+        primary: FloatProperty<T>,
+        secondary: FloatProperty<T>,
+        deviceProfile: DeviceProfile
+    ): Pair<FloatProperty<T>, FloatProperty<T>> = Pair(primary, secondary)
+
+    override fun getFloatingTaskOffscreenTranslationTarget(
+        floatingTask: View,
+        onScreenRect: RectF,
+        @StagePosition stagePosition: Int,
+        dp: DeviceProfile
+    ): Float = floatingTask.translationY - onScreenRect.height()
+
+    override fun setFloatingTaskPrimaryTranslation(
+        floatingTask: View,
+        translation: Float,
+        dp: DeviceProfile
+    ) {
+        floatingTask.translationY = translation
+    }
+
+    override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float =
+        floatingTask.translationY
+}
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 5cd9776..62dfd82 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -795,7 +795,7 @@
     }
 
     @Override
-    public Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) {
+    public float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) {
         return dp.isLeftRightSplit
                 ? floatingTask.getTranslationX()
                 : floatingTask.getTranslationY();
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
deleted file mode 100644
index 4b65d53..0000000
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.orientation;
-
-
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.ShapeDrawable;
-import android.util.FloatProperty;
-import android.util.Pair;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.util.SplitConfigurationOptions;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
-import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
-import com.android.quickstep.views.IconAppChipView;
-
-import java.util.List;
-
-/**
- * Abstraction layer to separate horizontal and vertical specific implementations
- * for {@link com.android.quickstep.views.RecentsView}. Majority of these implementations are
- * (should be) as simple as choosing the correct X and Y analogous methods.
- */
-public interface RecentsPagedOrientationHandler extends PagedOrientationHandler {
-
-    RecentsPagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler();
-    RecentsPagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler();
-    RecentsPagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler();
-
-    <T> void setSecondary(T target, Float2DAction<T> action, float param);
-    <T> void set(T target, Int2DAction<T> action, int primaryParam, int secondaryParam);
-    int getPrimarySize(View view);
-    float getPrimarySize(RectF rect);
-    int getSecondaryTranslationDirectionFactor();
-    float getDegreesRotated();
-    int getRotation();
-    boolean isLayoutNaturalToLauncher();
-
-    <T> T getPrimaryValue(T x, T y);
-    <T> T getSecondaryValue(T x, T y);
-    void setPrimaryScale(View view, float scale);
-    void setSecondaryScale(View view, float scale);
-    float getStart(RectF rect);
-    float getEnd(RectF rect);
-    int getClearAllSidePadding(View view, boolean isRtl);
-    int getSecondaryDimension(View view);
-    FloatProperty<View> getPrimaryViewTranslate();
-    FloatProperty<View> getSecondaryViewTranslate();
-    int getSplitTranslationDirectionFactor(@StagePosition int stagePosition,
-            DeviceProfile deviceProfile);
-    Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
-            FloatProperty secondary, DeviceProfile deviceProfile);
-    int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
-    List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
-    /**
-     * @param placeholderHeight height of placeholder view in portrait, width in landscape
-     */
-    void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset,
-            DeviceProfile dp, @StagePosition int stagePosition, Rect out);
-
-    /**
-     * Centers an icon in the split staging area, accounting for insets.
-     * @param out The icon that needs to be centered.
-     * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is
-     *                        offscreen).
-     * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is
-     *                        offscreen).
-     * @param fullscreenScaleX A x-scaling factor used to convert coordinates back into pixels.
-     * @param fullscreenScaleY A y-scaling factor used to convert coordinates back into pixels.
-     * @param drawableWidth The icon's drawable (final) width.
-     * @param drawableHeight The icon's drawable (final) height.
-     * @param dp The device profile, used to report rotation and hardware insets.
-     * @param stagePosition 0 if the staging area is pinned to top/left, 1 for bottom/right.
-     */
-    void updateSplitIconParams(View out, float onScreenRectCenterX,
-            float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY,
-            int drawableWidth, int drawableHeight, DeviceProfile dp,
-            @StagePosition int stagePosition);
-
-    /**
-     * Sets positioning and rotation for a SplitInstructionsView.
-     * @param out The SplitInstructionsView that needs to be positioned.
-     * @param dp The device profile, used to report rotation and device type.
-     * @param splitInstructionsHeight The SplitInstructionView's height.
-     * @param splitInstructionsWidth  The SplitInstructionView's width.
-     */
-    void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
-            int splitInstructionsWidth);
-
-    /**
-     * @param splitDividerSize height of split screen drag handle in portrait, width in landscape
-     * @param stagePosition the split position option (top/left, bottom/right) of the first
-     *                           task selected for entering split
-     * @param out1 the bounds for where the first selected app will be
-     * @param out2 the bounds for where the second selected app will be, complimentary to
-     *             {@param out1} based on {@param initialSplitOption}
-     */
-    void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
-            @StagePosition int stagePosition, Rect out1, Rect out2);
-
-    int getDefaultSplitPosition(DeviceProfile deviceProfile);
-
-    /**
-     * @param outRect This is expected to be the rect that has the dimensions for a non-split,
-     *                fullscreen task in overview. This will directly be modified.
-     * @param desiredStagePosition Which stage position (topLeft/rightBottom) we want to resize
-     *                           outRect for
-     */
-    void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo,
-            @SplitConfigurationOptions.StagePosition int desiredStagePosition);
-
-    void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
-            int parentWidth, int parentHeight,
-            SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl);
-
-    /**
-     * Creates two Points representing the dimensions of the two tasks in a GroupedTaskView
-     *
-     * @return first -> primary task snapshot, second -> secondary task snapshot.
-     * x -> width, y -> height
-     */
-    Pair<Point, Point> getGroupedTaskViewSizes(DeviceProfile dp, SplitBounds splitBoundsConfig,
-            int parentWidth, int parentHeight);
-
-    // Overview TaskMenuView methods
-    /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
-    void setTaskIconParams(FrameLayout.LayoutParams iconParams,
-            int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl);
-
-    /**
-     * Sets layout params on the children of an app chip. Only use this when app chip is enabled.
-     */
-    void setIconAppChipChildrenParams(
-            FrameLayout.LayoutParams iconParams, int chipChildMarginStart);
-
-    void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
-            FrameLayout.LayoutParams iconMenuParams,
-            int iconMenuMargin, int thumbnailTopMargin);
-    void setSplitIconParams(View primaryIconView, View secondaryIconView,
-            int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
-            int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
-            DeviceProfile deviceProfile, SplitBounds splitConfig);
-
-    /*
-     * The following two methods try to center the TaskMenuView in landscape by finding the center
-     * of the thumbnail view and then subtracting half of the taskMenu width. In this case, the
-     * taskMenu width is the same size as the thumbnail width (what got set below in
-     * getTaskMenuWidth()), so we directly use that in the calculations.
-     */
-    float getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile,
-            float taskInsetMargin, View taskViewIcon);
-    float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin, View taskViewIcon);
-    int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
-            @StagePosition int stagePosition);
-
-    int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, float taskMenuX,
-            float taskMenuY);
-    /**
-     * Sets linear layout orientation for {@link com.android.launcher3.popup.SystemShortcut} items
-     * inside task menu view.
-     */
-    void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
-            LinearLayout taskMenuLayout, int dividerSpacing,
-            ShapeDrawable dividerDrawable);
-    /**
-     * Sets layout param attributes for {@link com.android.launcher3.popup.SystemShortcut} child
-     * views inside task menu view.
-     */
-    void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
-            LinearLayout viewGroup, DeviceProfile deviceProfile);
-
-    /**
-     * Calculates the position where a Digital Wellbeing Banner should be placed on its parent
-     * TaskView.
-     * @return A Pair of Floats representing the proper x and y translations.
-     */
-    Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
-            int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
-            View[] thumbnailViews, int desiredTaskId, View banner);
-
-    // The following are only used by TaskViewTouchHandler.
-    /** @return Either VERTICAL or HORIZONTAL. */
-    SingleAxisSwipeDetector.Direction getUpDownSwipeDirection();
-    /** @return Given {@link #getUpDownSwipeDirection()}, whether POSITIVE or NEGATIVE is up. */
-    int getUpDirection(boolean isRtl);
-    /** @return Whether the displacement is going towards the top of the screen. */
-    boolean isGoingUp(float displacement, boolean isRtl);
-    /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
-    int getTaskDragDisplacementFactor(boolean isRtl);
-
-    /**
-     * Maps the velocity from the coordinate plane of the foreground app to that
-     * of Launcher's (which now will always be portrait)
-     */
-    void adjustFloatingIconStartVelocity(PointF velocity);
-
-    /**
-     * Ensures that outStartRect left bound is within the DeviceProfile's visual boundaries
-     * @param outStartRect The start rect that will directly be modified
-     */
-    void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile);
-
-    /**
-     * Determine the target translation for animating the FloatingTaskView out. This value could
-     * either be an x-coordinate or a y-coordinate, depending on which way the FloatingTaskView was
-     * docked.
-     *
-     * @param floatingTask The FloatingTaskView.
-     * @param onScreenRect The current on-screen dimensions of the FloatingTaskView.
-     * @param stagePosition STAGE_POSITION_TOP_OR_LEFT or STAGE_POSITION_BOTTOM_OR_RIGHT.
-     * @param dp The device profile.
-     * @return A float. When an animation translates the FloatingTaskView to this position, it will
-     * appear to tuck away off the edge of the screen.
-     */
-    float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect,
-            @StagePosition int stagePosition, DeviceProfile dp);
-
-    /**
-     * Sets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
-     * either x or y), depending on how the view is oriented.
-     *
-     * @param floatingTask The FloatingTaskView to be translated.
-     * @param translation The target translation value.
-     * @param dp The current device profile.
-     */
-    void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, DeviceProfile dp);
-
-    /**
-     * Gets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
-     * either x or y), depending on how the view is oriented.
-     *
-     * @param floatingTask The FloatingTaskView in question.
-     * @param dp The current device profile.
-     * @return The current translation value.
-     */
-    Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp);
-}
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
new file mode 100644
index 0000000..6c82890
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.orientation
+
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.ShapeDrawable
+import android.util.FloatProperty
+import android.util.Pair
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.touch.PagedOrientationHandler
+import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction
+import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
+import com.android.quickstep.views.IconAppChipView
+
+/**
+ * Abstraction layer to separate horizontal and vertical specific implementations for
+ * [com.android.quickstep.views.RecentsView]. Majority of these implementations are (should be) as
+ * simple as choosing the correct X and Y analogous methods.
+ */
+interface RecentsPagedOrientationHandler : PagedOrientationHandler {
+    fun <T> setSecondary(target: T, action: Float2DAction<T>, param: Float)
+
+    operator fun <T> set(target: T, action: Int2DAction<T>, primaryParam: Int, secondaryParam: Int)
+
+    fun getPrimarySize(view: View): Int
+
+    fun getPrimarySize(rect: RectF): Float
+
+    val secondaryTranslationDirectionFactor: Int
+
+    val degreesRotated: Float
+
+    val rotation: Int
+
+    val isLayoutNaturalToLauncher: Boolean
+
+    fun <T> getPrimaryValue(x: T, y: T): T
+
+    fun <T> getSecondaryValue(x: T, y: T): T
+
+    fun setPrimaryScale(view: View, scale: Float)
+
+    fun setSecondaryScale(view: View, scale: Float)
+
+    fun getStart(rect: RectF): Float
+
+    fun getEnd(rect: RectF): Float
+
+    fun getClearAllSidePadding(view: View, isRtl: Boolean): Int
+
+    fun getSecondaryDimension(view: View): Int
+
+    val primaryViewTranslate: FloatProperty<View>
+    val secondaryViewTranslate: FloatProperty<View>
+
+    fun getSplitTranslationDirectionFactor(
+        @StagePosition stagePosition: Int,
+        deviceProfile: DeviceProfile
+    ): Int
+
+    fun <T> getSplitSelectTaskOffset(
+        primary: FloatProperty<T>,
+        secondary: FloatProperty<T>,
+        deviceProfile: DeviceProfile
+    ): Pair<FloatProperty<T>, FloatProperty<T>>
+
+    fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int
+
+    fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption>
+
+    /** @param placeholderHeight height of placeholder view in portrait, width in landscape */
+    fun getInitialSplitPlaceholderBounds(
+        placeholderHeight: Int,
+        placeholderInset: Int,
+        dp: DeviceProfile,
+        @StagePosition stagePosition: Int,
+        out: Rect
+    )
+
+    /**
+     * Centers an icon in the split staging area, accounting for insets.
+     *
+     * @param out The icon that needs to be centered.
+     * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is
+     *   offscreen).
+     * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is
+     *   offscreen).
+     * @param fullscreenScaleX A x-scaling factor used to convert coordinates back into pixels.
+     * @param fullscreenScaleY A y-scaling factor used to convert coordinates back into pixels.
+     * @param drawableWidth The icon's drawable (final) width.
+     * @param drawableHeight The icon's drawable (final) height.
+     * @param dp The device profile, used to report rotation and hardware insets.
+     * @param stagePosition 0 if the staging area is pinned to top/left, 1 for bottom/right.
+     */
+    fun updateSplitIconParams(
+        out: View,
+        onScreenRectCenterX: Float,
+        onScreenRectCenterY: Float,
+        fullscreenScaleX: Float,
+        fullscreenScaleY: Float,
+        drawableWidth: Int,
+        drawableHeight: Int,
+        dp: DeviceProfile,
+        @StagePosition stagePosition: Int
+    )
+
+    /**
+     * Sets positioning and rotation for a SplitInstructionsView.
+     *
+     * @param out The SplitInstructionsView that needs to be positioned.
+     * @param dp The device profile, used to report rotation and device type.
+     * @param splitInstructionsHeight The SplitInstructionView's height.
+     * @param splitInstructionsWidth The SplitInstructionView's width.
+     */
+    fun setSplitInstructionsParams(
+        out: View,
+        dp: DeviceProfile,
+        splitInstructionsHeight: Int,
+        splitInstructionsWidth: Int
+    )
+
+    /**
+     * @param splitDividerSize height of split screen drag handle in portrait, width in landscape
+     * @param stagePosition the split position option (top/left, bottom/right) of the first task
+     *   selected for entering split
+     * @param out1 the bounds for where the first selected app will be
+     * @param out2 the bounds for where the second selected app will be, complimentary to {@param
+     *   out1} based on {@param initialSplitOption}
+     */
+    fun getFinalSplitPlaceholderBounds(
+        splitDividerSize: Int,
+        dp: DeviceProfile,
+        @StagePosition stagePosition: Int,
+        out1: Rect,
+        out2: Rect
+    )
+
+    fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int
+
+    /**
+     * @param outRect This is expected to be the rect that has the dimensions for a non-split,
+     *   fullscreen task in overview. This will directly be modified.
+     * @param desiredStagePosition Which stage position (topLeft/rightBottom) we want to resize
+     *   outRect for
+     */
+    fun setSplitTaskSwipeRect(
+        dp: DeviceProfile,
+        outRect: Rect,
+        splitInfo: SplitConfigurationOptions.SplitBounds,
+        @StagePosition desiredStagePosition: Int
+    )
+
+    fun measureGroupedTaskViewThumbnailBounds(
+        primarySnapshot: View,
+        secondarySnapshot: View,
+        parentWidth: Int,
+        parentHeight: Int,
+        splitBoundsConfig: SplitConfigurationOptions.SplitBounds,
+        dp: DeviceProfile,
+        isRtl: Boolean
+    )
+
+    /**
+     * Creates two Points representing the dimensions of the two tasks in a GroupedTaskView
+     *
+     * @return first -> primary task snapshot, second -> secondary task snapshot. x -> width, y ->
+     *   height
+     */
+    fun getGroupedTaskViewSizes(
+        dp: DeviceProfile,
+        splitBoundsConfig: SplitConfigurationOptions.SplitBounds,
+        parentWidth: Int,
+        parentHeight: Int
+    ): Pair<Point, Point>
+    // Overview TaskMenuView methods
+    /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
+    fun setTaskIconParams(
+        iconParams: FrameLayout.LayoutParams,
+        taskIconMargin: Int,
+        taskIconHeight: Int,
+        thumbnailTopMargin: Int,
+        isRtl: Boolean
+    )
+
+    /**
+     * Sets layout params on the children of an app chip. Only use this when app chip is enabled.
+     */
+    fun setIconAppChipChildrenParams(
+        iconParams: FrameLayout.LayoutParams,
+        chipChildMarginStart: Int
+    )
+
+    fun setIconAppChipMenuParams(
+        iconAppChipView: IconAppChipView,
+        iconMenuParams: FrameLayout.LayoutParams,
+        iconMenuMargin: Int,
+        thumbnailTopMargin: Int
+    )
+
+    fun setSplitIconParams(
+        primaryIconView: View,
+        secondaryIconView: View,
+        taskIconHeight: Int,
+        primarySnapshotWidth: Int,
+        primarySnapshotHeight: Int,
+        groupedTaskViewHeight: Int,
+        groupedTaskViewWidth: Int,
+        isRtl: Boolean,
+        deviceProfile: DeviceProfile,
+        splitConfig: SplitConfigurationOptions.SplitBounds
+    )
+
+    /*
+     * The following two methods try to center the TaskMenuView in landscape by finding the center
+     * of the thumbnail view and then subtracting half of the taskMenu width. In this case, the
+     * taskMenu width is the same size as the thumbnail width (what got set below in
+     * getTaskMenuWidth()), so we directly use that in the calculations.
+     */
+    fun getTaskMenuX(
+        x: Float,
+        thumbnailView: View,
+        deviceProfile: DeviceProfile,
+        taskInsetMargin: Float,
+        taskViewIcon: View
+    ): Float
+
+    fun getTaskMenuY(
+        y: Float,
+        thumbnailView: View,
+        stagePosition: Int,
+        taskMenuView: View,
+        taskInsetMargin: Float,
+        taskViewIcon: View
+    ): Float
+
+    fun getTaskMenuWidth(
+        thumbnailView: View,
+        deviceProfile: DeviceProfile,
+        @StagePosition stagePosition: Int
+    ): Int
+
+    fun getTaskMenuHeight(
+        taskInsetMargin: Float,
+        deviceProfile: DeviceProfile,
+        taskMenuX: Float,
+        taskMenuY: Float
+    ): Int
+
+    /**
+     * Sets linear layout orientation for [com.android.launcher3.popup.SystemShortcut] items inside
+     * task menu view.
+     */
+    fun setTaskOptionsMenuLayoutOrientation(
+        deviceProfile: DeviceProfile,
+        taskMenuLayout: LinearLayout,
+        dividerSpacing: Int,
+        dividerDrawable: ShapeDrawable
+    )
+
+    /**
+     * Sets layout param attributes for [com.android.launcher3.popup.SystemShortcut] child views
+     * inside task menu view.
+     */
+    fun setLayoutParamsForTaskMenuOptionItem(
+        lp: LinearLayout.LayoutParams,
+        viewGroup: LinearLayout,
+        deviceProfile: DeviceProfile
+    )
+
+    /**
+     * Calculates the position where a Digital Wellbeing Banner should be placed on its parent
+     * TaskView.
+     *
+     * @return A Pair of Floats representing the proper x and y translations.
+     */
+    fun getDwbLayoutTranslations(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        splitBounds: SplitConfigurationOptions.SplitBounds?,
+        deviceProfile: DeviceProfile,
+        thumbnailViews: Array<View>,
+        desiredTaskId: Int,
+        banner: View
+    ): Pair<Float, Float>
+    // The following are only used by TaskViewTouchHandler.
+
+    /** @return Either VERTICAL or HORIZONTAL. */
+    val upDownSwipeDirection: SingleAxisSwipeDetector.Direction
+
+    /** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is up. */
+    fun getUpDirection(isRtl: Boolean): Int
+
+    /** @return Whether the displacement is going towards the top of the screen. */
+    fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean
+
+    /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
+    fun getTaskDragDisplacementFactor(isRtl: Boolean): Int
+
+    /**
+     * Maps the velocity from the coordinate plane of the foreground app to that of Launcher's
+     * (which now will always be portrait)
+     */
+    fun adjustFloatingIconStartVelocity(velocity: PointF)
+
+    /**
+     * Ensures that outStartRect left bound is within the DeviceProfile's visual boundaries
+     *
+     * @param outStartRect The start rect that will directly be modified
+     */
+    fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile)
+
+    /**
+     * Determine the target translation for animating the FloatingTaskView out. This value could
+     * either be an x-coordinate or a y-coordinate, depending on which way the FloatingTaskView was
+     * docked.
+     *
+     * @param floatingTask The FloatingTaskView.
+     * @param onScreenRect The current on-screen dimensions of the FloatingTaskView.
+     * @param stagePosition STAGE_POSITION_TOP_OR_LEFT or STAGE_POSITION_BOTTOM_OR_RIGHT.
+     * @param dp The device profile.
+     * @return A float. When an animation translates the FloatingTaskView to this position, it will
+     *   appear to tuck away off the edge of the screen.
+     */
+    fun getFloatingTaskOffscreenTranslationTarget(
+        floatingTask: View,
+        onScreenRect: RectF,
+        @StagePosition stagePosition: Int,
+        dp: DeviceProfile
+    ): Float
+
+    /**
+     * Sets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
+     * either x or y), depending on how the view is oriented.
+     *
+     * @param floatingTask The FloatingTaskView to be translated.
+     * @param translation The target translation value.
+     * @param dp The current device profile.
+     */
+    fun setFloatingTaskPrimaryTranslation(floatingTask: View, translation: Float, dp: DeviceProfile)
+
+    /**
+     * Gets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
+     * either x or y), depending on how the view is oriented.
+     *
+     * @param floatingTask The FloatingTaskView in question.
+     * @param dp The current device profile.
+     * @return The current translation value.
+     */
+    fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float
+
+    companion object {
+        @JvmField val PORTRAIT: RecentsPagedOrientationHandler = PortraitPagedViewHandler()
+        @JvmField val LANDSCAPE: RecentsPagedOrientationHandler = LandscapePagedViewHandler()
+        @JvmField val SEASCAPE: RecentsPagedOrientationHandler = SeascapePagedViewHandler()
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
deleted file mode 100644
index 89c678c..0000000
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
+++ /dev/null
@@ -1,418 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.orientation;
-
-import static android.view.Gravity.BOTTOM;
-import static android.view.Gravity.CENTER_VERTICAL;
-import static android.view.Gravity.END;
-import static android.view.Gravity.RIGHT;
-import static android.view.Gravity.START;
-import static android.view.Gravity.TOP;
-import static android.view.View.LAYOUT_DIRECTION_RTL;
-
-import static com.android.launcher3.Flags.enableOverviewIconMenu;
-import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
-
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.Surface;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
-import com.android.launcher3.util.SplitConfigurationOptions;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.views.IconAppChipView;
-
-import java.util.Collections;
-import java.util.List;
-
-public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
-
-    @Override
-    public int getSecondaryTranslationDirectionFactor() {
-        return -1;
-    }
-
-    @Override
-    public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
-        if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
-            return -1;
-        } else {
-            return 1;
-        }
-    }
-
-    @Override
-    public boolean getRecentsRtlSetting(Resources resources) {
-        return Utilities.isRtl(resources);
-    }
-
-    @Override
-    public float getDegreesRotated() {
-        return 270;
-    }
-
-    @Override
-    public int getRotation() {
-        return Surface.ROTATION_270;
-    }
-
-    @Override
-    public void adjustFloatingIconStartVelocity(PointF velocity) {
-        float oldX = velocity.x;
-        float oldY = velocity.y;
-        velocity.set(oldY, -oldX);
-    }
-
-    @Override
-    public float getTaskMenuX(float x, View thumbnailView,
-            DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
-        return x + taskInsetMargin;
-    }
-
-    @Override
-    public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
-            View taskMenuView, float taskInsetMargin, View taskViewIcon) {
-        if (enableOverviewIconMenu()) {
-            return y;
-        }
-        BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
-        int taskMenuWidth = lp.width;
-        if (stagePosition == STAGE_POSITION_UNDEFINED) {
-            return y + taskInsetMargin
-                    + (thumbnailView.getMeasuredHeight() + taskMenuWidth) / 2f;
-        } else {
-            return y + taskMenuWidth + taskInsetMargin;
-        }
-    }
-
-    @Override
-    public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile,
-            float taskMenuX, float taskMenuY) {
-        return (int) (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX);
-    }
-
-    @Override
-    public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo,
-            int desiredStagePosition) {
-        float topLeftTaskPercent = splitInfo.appsStackedVertically
-                ? splitInfo.topTaskPercent
-                : splitInfo.leftTaskPercent;
-        float dividerBarPercent = splitInfo.appsStackedVertically
-                ? splitInfo.dividerHeightPercent
-                : splitInfo.dividerWidthPercent;
-
-        // In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of
-        // the screen. This is to preserve consistency when the user rotates: From the user's POV,
-        // the primary should always be on the left.
-        if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) {
-            outRect.top += (int) (outRect.height() * ((1 - topLeftTaskPercent)));
-        } else {
-            outRect.bottom -= (int) (outRect.height() * (topLeftTaskPercent + dividerBarPercent));
-        }
-    }
-
-    @Override
-    public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
-            int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
-            View[] thumbnailViews, int desiredTaskId, View banner) {
-        boolean isRtl = banner.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        float translationX = 0;
-        float translationY = 0;
-        FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
-        banner.setPivotX(0);
-        banner.setPivotY(0);
-        banner.setRotation(getDegreesRotated());
-        translationX = taskViewWidth - banner.getHeight();
-        FrameLayout.LayoutParams snapshotParams =
-                (FrameLayout.LayoutParams) thumbnailViews[0]
-                        .getLayoutParams();
-        bannerParams.gravity = BOTTOM | (isRtl ? END : START);
-
-        if (splitBounds == null) {
-            // Single, fullscreen case
-            bannerParams.width = taskViewHeight - snapshotParams.topMargin;
-            translationY = banner.getHeight();
-            return new Pair<>(translationX, translationY);
-        }
-
-        // Set correct width
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            bannerParams.width = thumbnailViews[0].getMeasuredHeight();
-        } else {
-            bannerParams.width = thumbnailViews[1].getMeasuredHeight();
-        }
-
-        // Set translations
-        if (desiredTaskId == splitBounds.rightBottomTaskId) {
-            translationY = banner.getHeight();
-        }
-        if (desiredTaskId == splitBounds.leftTopTaskId) {
-            float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
-                    ? (1f - splitBounds.topTaskPercent)
-                    : (1f - splitBounds.leftTaskPercent);
-            translationY = banner.getHeight()
-                    - ((taskViewHeight - snapshotParams.topMargin)
-                    * bottomRightTaskPlusDividerPercent);
-        }
-        return new Pair<>(translationX, translationY);
-    }
-
-    @Override
-    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
-        return dp.widthPx - rect.right;
-    }
-
-    @Override
-    public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
-        // Add "right" option which is actually the top
-        return Collections.singletonList(new SplitPositionOption(
-                R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen,
-                STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
-    }
-
-    @Override
-    public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
-            int splitInstructionsWidth) {
-        out.setPivotX(0);
-        out.setPivotY(splitInstructionsHeight);
-        out.setRotation(getDegreesRotated());
-        int distanceToEdge = out.getResources().getDimensionPixelSize(
-                R.dimen.split_instructions_bottom_margin_phone_landscape);
-        // Adjust for any insets on the right edge
-        int insetCorrectionX = dp.getInsets().right;
-        // Center the view in case of unbalanced insets on top or bottom of screen
-        int insetCorrectionY = (dp.getInsets().bottom - dp.getInsets().top) / 2;
-        out.setTranslationX(splitInstructionsWidth - distanceToEdge + insetCorrectionX);
-        out.setTranslationY(((-splitInstructionsHeight + splitInstructionsWidth) / 2f)
-                + insetCorrectionY);
-        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
-        // Setting gravity to RIGHT instead of the lint-recommended END because we always want this
-        // view to be screen-right when phone is in seascape, regardless of the RtL setting.
-        lp.gravity = RIGHT | CENTER_VERTICAL;
-        out.setLayoutParams(lp);
-    }
-
-    @Override
-    public void setTaskIconParams(FrameLayout.LayoutParams iconParams,
-            int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
-        iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
-        iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0);
-    }
-
-    @Override
-    public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
-            int chipChildMarginStart) {
-        iconParams.setMargins(0, 0, 0, 0);
-        iconParams.setMarginStart(chipChildMarginStart);
-        iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
-    }
-
-    @Override
-    public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
-            FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
-        boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        iconMenuParams.gravity = (isRtl ? TOP : BOTTOM) | (isRtl ? END : START);
-        iconMenuParams.setMarginStart(0);
-        iconMenuParams.topMargin = isRtl ? iconMenuMargin : 0;
-        iconMenuParams.bottomMargin = 0;
-        iconMenuParams.setMarginEnd(isRtl ? thumbnailTopMargin : 0);
-
-        // Use half menu height to place the pivot within the X/Y center of icon in the menu.
-        float iconCenter = iconAppChipView.getHeight() / 2f;
-        iconAppChipView.setPivotX(isRtl ? iconMenuParams.width / 2f : iconCenter);
-        iconAppChipView.setPivotY(
-                isRtl ? iconMenuParams.width / 2f : iconCenter - iconMenuMargin);
-        iconAppChipView.setSplitTranslationY(0);
-        iconAppChipView.setRotation(getDegreesRotated());
-    }
-
-    @Override
-    public void setSplitIconParams(View primaryIconView, View secondaryIconView,
-            int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
-            int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
-            DeviceProfile deviceProfile, SplitBounds splitConfig) {
-        super.setSplitIconParams(primaryIconView, secondaryIconView, taskIconHeight,
-                primarySnapshotWidth, primarySnapshotHeight, groupedTaskViewHeight,
-                groupedTaskViewWidth, isRtl, deviceProfile, splitConfig);
-        FrameLayout.LayoutParams primaryIconParams =
-                (FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
-        FrameLayout.LayoutParams secondaryIconParams =
-                (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams();
-
-        // We calculate the "midpoint" of the thumbnail area, and place the icons there.
-        // This is the place where the thumbnail area splits by default, in a near-50/50 split.
-        // It is usually not exactly 50/50, due to insets/screen cutouts.
-        int fullscreenInsetThickness = deviceProfile.getInsets().top
-                - deviceProfile.getInsets().bottom;
-        int fullscreenMidpointFromBottom = ((deviceProfile.heightPx
-                - fullscreenInsetThickness) / 2);
-        float midpointFromBottomPct = (float) fullscreenMidpointFromBottom / deviceProfile.heightPx;
-        float insetPct = (float) fullscreenInsetThickness / deviceProfile.heightPx;
-        int spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx;
-        int overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots;
-        int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct);
-        int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct);
-
-        int gravity = (isRtl ? TOP : BOTTOM) | (isRtl ? END : START);
-        primaryIconParams.gravity = gravity;
-        secondaryIconParams.gravity = gravity;
-        primaryIconView.setTranslationX(0);
-        secondaryIconView.setTranslationX(0);
-        if (enableOverviewIconMenu()) {
-            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
-            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
-            if (isRtl) {
-                primaryAppChipView.setSplitTranslationY(
-                        groupedTaskViewHeight - primarySnapshotHeight);
-                secondaryAppChipView.setSplitTranslationY(0);
-            } else {
-                secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
-                primaryAppChipView.setSplitTranslationY(0);
-            }
-        } else if (splitConfig.initiatedFromSeascape) {
-            // if the split was initiated from seascape,
-            // the task on the right (secondary) is slightly larger
-            if (isRtl) {
-                primaryIconView.setTranslationY(bottomToMidpointOffset - insetOffset
-                        + taskIconHeight);
-                secondaryIconView.setTranslationY(bottomToMidpointOffset - insetOffset);
-            } else {
-                primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset
-                        + taskIconHeight);
-                secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset);
-            }
-        } else {
-            // if not,
-            // the task on the left (primary) is slightly larger
-            if (isRtl) {
-                primaryIconView.setTranslationY(bottomToMidpointOffset + taskIconHeight);
-                secondaryIconView.setTranslationY(bottomToMidpointOffset);
-            } else {
-                primaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight);
-                secondaryIconView.setTranslationY(-bottomToMidpointOffset);
-            }
-        }
-
-        primaryIconView.setLayoutParams(primaryIconParams);
-        secondaryIconView.setLayoutParams(secondaryIconParams);
-    }
-
-    @Override
-    public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
-            int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp,
-            boolean isRtl) {
-        FrameLayout.LayoutParams primaryParams =
-                (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
-        FrameLayout.LayoutParams secondaryParams =
-                (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
-
-        // Swap the margins that are set in TaskView#setRecentsOrientedState()
-        secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx;
-        primaryParams.topMargin = 0;
-
-        // Measure and layout the thumbnails bottom up, since the primary is on the visual left
-        // (portrait bottom) and secondary is on the right (portrait top)
-        int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
-        int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
-        int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.dividerHeightPercent
-                : splitBoundsConfig.dividerWidthPercent));
-
-        Pair<Point, Point> taskViewSizes =
-                getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
-
-        secondarySnapshot.setTranslationY(0);
-        primarySnapshot.setTranslationY(taskViewSizes.second.y + spaceAboveSnapshot + dividerBar);
-
-        primarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)
-        );
-        secondarySnapshot.measure(
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY)
-        );
-    }
-
-    @Override
-    public Pair<Point, Point> getGroupedTaskViewSizes(
-            DeviceProfile dp,
-            SplitBounds splitBoundsConfig,
-            int parentWidth,
-            int parentHeight) {
-        // Measure and layout the thumbnails bottom up, since the primary is on the visual left
-        // (portrait bottom) and secondary is on the right (portrait top)
-        int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
-        int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
-        int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.dividerHeightPercent
-                : splitBoundsConfig.dividerWidthPercent));
-        float taskPercent = splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.topTaskPercent
-                : splitBoundsConfig.leftTaskPercent;
-
-        Point firstTaskViewSize = new Point(
-                parentWidth,
-                (int) (totalThumbnailHeight * taskPercent)
-        );
-        Point secondTaskViewSize = new Point(
-                parentWidth,
-                totalThumbnailHeight - firstTaskViewSize.y - dividerBar
-        );
-
-        return new Pair<>(firstTaskViewSize, secondTaskViewSize);
-    }
-
-    /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
-
-    @Override
-    public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
-        return HORIZONTAL;
-    }
-
-    @Override
-    public int getUpDirection(boolean isRtl) {
-        return isRtl ? SingleAxisSwipeDetector.DIRECTION_POSITIVE
-                : SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
-    }
-
-    @Override
-    public boolean isGoingUp(float displacement, boolean isRtl) {
-        return isRtl ? displacement > 0 : displacement < 0;
-    }
-
-    @Override
-    public int getTaskDragDisplacementFactor(boolean isRtl) {
-        return isRtl ? -1 : 1;
-    }
-
-    /* -------------------- */
-}
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
new file mode 100644
index 0000000..0c78b8f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.orientation
+
+import android.content.res.Resources
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.util.Pair
+import android.view.Gravity
+import android.view.Surface
+import android.view.View
+import android.view.View.MeasureSpec
+import android.widget.FrameLayout
+import androidx.core.util.component1
+import androidx.core.util.component2
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Flags
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.SplitConfigurationOptions.*
+import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.views.IconAppChipView
+
+class SeascapePagedViewHandler : LandscapePagedViewHandler() {
+    override val secondaryTranslationDirectionFactor: Int = -1
+
+    override fun getSplitTranslationDirectionFactor(
+        stagePosition: Int,
+        deviceProfile: DeviceProfile
+    ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1
+
+    override fun getRecentsRtlSetting(resources: Resources): Boolean = Utilities.isRtl(resources)
+
+    override val degreesRotated: Float = 270f
+
+    override val rotation: Int = Surface.ROTATION_270
+
+    override fun adjustFloatingIconStartVelocity(velocity: PointF) =
+        velocity.set(velocity.y, -velocity.x)
+
+    override fun getTaskMenuX(
+        x: Float,
+        thumbnailView: View,
+        deviceProfile: DeviceProfile,
+        taskInsetMargin: Float,
+        taskViewIcon: View
+    ): Float = x + taskInsetMargin
+
+    override fun getTaskMenuY(
+        y: Float,
+        thumbnailView: View,
+        stagePosition: Int,
+        taskMenuView: View,
+        taskInsetMargin: Float,
+        taskViewIcon: View
+    ): Float {
+        if (Flags.enableOverviewIconMenu()) {
+            return y
+        }
+        val lp = taskMenuView.layoutParams as BaseDragLayer.LayoutParams
+        val taskMenuWidth = lp.width
+        return if (stagePosition == STAGE_POSITION_UNDEFINED) {
+            y + taskInsetMargin + (thumbnailView.measuredHeight + taskMenuWidth) / 2f
+        } else {
+            y + taskMenuWidth + taskInsetMargin
+        }
+    }
+
+    override fun getTaskMenuHeight(
+        taskInsetMargin: Float,
+        deviceProfile: DeviceProfile,
+        taskMenuX: Float,
+        taskMenuY: Float
+    ): Int = (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX).toInt()
+
+    override fun setSplitTaskSwipeRect(
+        dp: DeviceProfile,
+        outRect: Rect,
+        splitInfo: SplitBounds,
+        desiredStagePosition: Int
+    ) {
+        val topLeftTaskPercent: Float
+        val dividerBarPercent: Float
+        if (splitInfo.appsStackedVertically) {
+            topLeftTaskPercent = splitInfo.topTaskPercent
+            dividerBarPercent = splitInfo.dividerHeightPercent
+        } else {
+            topLeftTaskPercent = splitInfo.leftTaskPercent
+            dividerBarPercent = splitInfo.dividerWidthPercent
+        }
+
+        // In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of
+        // the screen. This is to preserve consistency when the user rotates: From the user's POV,
+        // the primary should always be on the left.
+        if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+            outRect.top += (outRect.height() * (1 - topLeftTaskPercent)).toInt()
+        } else {
+            outRect.bottom -= (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt()
+        }
+    }
+
+    override fun getDwbLayoutTranslations(
+        taskViewWidth: Int,
+        taskViewHeight: Int,
+        splitBounds: SplitBounds?,
+        deviceProfile: DeviceProfile,
+        thumbnailViews: Array<View>,
+        desiredTaskId: Int,
+        banner: View
+    ): Pair<Float, Float> {
+        val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
+        val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
+
+        val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
+        bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START
+        banner.pivotX = 0f
+        banner.pivotY = 0f
+        banner.rotation = degreesRotated
+
+        val translationX: Float = (taskViewWidth - banner.height).toFloat()
+        if (splitBounds == null) {
+            // Single, fullscreen case
+            bannerParams.width = taskViewHeight - snapshotParams.topMargin
+            return Pair(translationX, banner.height.toFloat())
+        }
+
+        // Set correct width and translations
+        val translationY: Float
+        if (desiredTaskId == splitBounds.leftTopTaskId) {
+            bannerParams.width = thumbnailViews[0].measuredHeight
+            val bottomRightTaskPlusDividerPercent =
+                if (splitBounds.appsStackedVertically) {
+                    1f - splitBounds.topTaskPercent
+                } else {
+                    1f - splitBounds.leftTaskPercent
+                }
+            translationY =
+                banner.height -
+                    (taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent
+        } else {
+            bannerParams.width = thumbnailViews[1].measuredHeight
+            translationY = banner.height.toFloat()
+        }
+
+        return Pair(translationX, translationY)
+    }
+
+    override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int =
+        dp.widthPx - rect.right
+
+    override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> =
+        // Add "right" option which is actually the top
+        listOf(
+            SplitPositionOption(
+                R.drawable.ic_split_horizontal,
+                R.string.recent_task_option_split_screen,
+                STAGE_POSITION_BOTTOM_OR_RIGHT,
+                STAGE_TYPE_MAIN
+            )
+        )
+
+    override fun setSplitInstructionsParams(
+        out: View,
+        dp: DeviceProfile,
+        splitInstructionsHeight: Int,
+        splitInstructionsWidth: Int
+    ) {
+        out.pivotX = 0f
+        out.pivotY = splitInstructionsHeight.toFloat()
+        out.rotation = degreesRotated
+        val distanceToEdge =
+            out.resources.getDimensionPixelSize(
+                R.dimen.split_instructions_bottom_margin_phone_landscape
+            )
+        // Adjust for any insets on the right edge
+        val insetCorrectionX = dp.insets.right
+        // Center the view in case of unbalanced insets on top or bottom of screen
+        val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2
+        out.translationX = (splitInstructionsWidth - distanceToEdge + insetCorrectionX).toFloat()
+        out.translationY =
+            (-splitInstructionsHeight + splitInstructionsWidth) / 2f + insetCorrectionY
+        // Setting gravity to RIGHT instead of the lint-recommended END because we always want this
+        // view to be screen-right when phone is in seascape, regardless of the RtL setting.
+        val lp = out.layoutParams as FrameLayout.LayoutParams
+        lp.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
+        out.layoutParams = lp
+    }
+
+    override fun setTaskIconParams(
+        iconParams: FrameLayout.LayoutParams,
+        taskIconMargin: Int,
+        taskIconHeight: Int,
+        thumbnailTopMargin: Int,
+        isRtl: Boolean
+    ) {
+        iconParams.gravity =
+            if (isRtl) {
+                Gravity.END or Gravity.CENTER_VERTICAL
+            } else {
+                Gravity.START or Gravity.CENTER_VERTICAL
+            }
+        iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0)
+    }
+
+    override fun setIconAppChipChildrenParams(
+        iconParams: FrameLayout.LayoutParams,
+        chipChildMarginStart: Int
+    ) {
+        iconParams.setMargins(0, 0, 0, 0)
+        iconParams.marginStart = chipChildMarginStart
+        iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
+    }
+
+    override fun setIconAppChipMenuParams(
+        iconAppChipView: IconAppChipView,
+        iconMenuParams: FrameLayout.LayoutParams,
+        iconMenuMargin: Int,
+        thumbnailTopMargin: Int
+    ) {
+        val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL
+        val iconCenter = iconAppChipView.getHeight() / 2f
+
+        if (isRtl) {
+            iconMenuParams.gravity = Gravity.TOP or Gravity.END
+            iconMenuParams.topMargin = iconMenuMargin
+            iconMenuParams.marginEnd = thumbnailTopMargin
+            // Use half menu height to place the pivot within the X/Y center of icon in the menu.
+            iconAppChipView.pivotX = iconMenuParams.width / 2f
+            iconAppChipView.pivotY = iconMenuParams.width / 2f
+        } else {
+            iconMenuParams.gravity = Gravity.BOTTOM or Gravity.START
+            iconMenuParams.topMargin = 0
+            iconMenuParams.marginEnd = 0
+            iconAppChipView.pivotX = iconCenter
+            iconAppChipView.pivotY = iconCenter - iconMenuMargin
+        }
+        iconMenuParams.marginStart = 0
+        iconMenuParams.bottomMargin = 0
+        iconAppChipView.setSplitTranslationY(0f)
+        iconAppChipView.setRotation(degreesRotated)
+    }
+
+    override fun setSplitIconParams(
+        primaryIconView: View,
+        secondaryIconView: View,
+        taskIconHeight: Int,
+        primarySnapshotWidth: Int,
+        primarySnapshotHeight: Int,
+        groupedTaskViewHeight: Int,
+        groupedTaskViewWidth: Int,
+        isRtl: Boolean,
+        deviceProfile: DeviceProfile,
+        splitConfig: SplitBounds
+    ) {
+        super.setSplitIconParams(
+            primaryIconView,
+            secondaryIconView,
+            taskIconHeight,
+            primarySnapshotWidth,
+            primarySnapshotHeight,
+            groupedTaskViewHeight,
+            groupedTaskViewWidth,
+            isRtl,
+            deviceProfile,
+            splitConfig
+        )
+        val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams
+        val secondaryIconParams = secondaryIconView.layoutParams as FrameLayout.LayoutParams
+
+        // We calculate the "midpoint" of the thumbnail area, and place the icons there.
+        // This is the place where the thumbnail area splits by default, in a near-50/50 split.
+        // It is usually not exactly 50/50, due to insets/screen cutouts.
+        val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom)
+        val fullscreenMidpointFromBottom = (deviceProfile.heightPx - fullscreenInsetThickness) / 2
+        val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx
+        val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx
+        val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx
+        val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots
+        val bottomToMidpointOffset =
+            (overviewThumbnailAreaThickness * midpointFromBottomPct).toInt()
+        val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt()
+        val gravity = if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START
+        primaryIconParams.gravity = gravity
+        secondaryIconParams.gravity = gravity
+        primaryIconView.translationX = 0f
+        secondaryIconView.translationX = 0f
+        when {
+            Flags.enableOverviewIconMenu() -> {
+                val primaryAppChipView = primaryIconView as IconAppChipView
+                val secondaryAppChipView = secondaryIconView as IconAppChipView
+                if (isRtl) {
+                    primaryAppChipView.setSplitTranslationY(
+                        (groupedTaskViewHeight - primarySnapshotHeight).toFloat()
+                    )
+                    secondaryAppChipView.setSplitTranslationY(0f)
+                } else {
+                    secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat())
+                    primaryAppChipView.setSplitTranslationY(0f)
+                }
+            }
+            splitConfig.initiatedFromSeascape -> {
+                // if the split was initiated from seascape,
+                // the task on the right (secondary) is slightly larger
+                if (isRtl) {
+                    primaryIconView.translationY =
+                        (bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
+                    secondaryIconView.translationY =
+                        (bottomToMidpointOffset - insetOffset).toFloat()
+                } else {
+                    primaryIconView.translationY =
+                        (-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
+                    secondaryIconView.translationY =
+                        (-bottomToMidpointOffset - insetOffset).toFloat()
+                }
+            }
+            else -> {
+                // if not,
+                // the task on the left (primary) is slightly larger
+                if (isRtl) {
+                    primaryIconView.translationY =
+                        (bottomToMidpointOffset + taskIconHeight).toFloat()
+                    secondaryIconView.translationY = bottomToMidpointOffset.toFloat()
+                } else {
+                    primaryIconView.translationY =
+                        (-bottomToMidpointOffset + taskIconHeight).toFloat()
+                    secondaryIconView.translationY = -bottomToMidpointOffset.toFloat()
+                }
+            }
+        }
+        primaryIconView.layoutParams = primaryIconParams
+        secondaryIconView.layoutParams = secondaryIconParams
+    }
+
+    override fun measureGroupedTaskViewThumbnailBounds(
+        primarySnapshot: View,
+        secondarySnapshot: View,
+        parentWidth: Int,
+        parentHeight: Int,
+        splitBoundsConfig: SplitBounds,
+        dp: DeviceProfile,
+        isRtl: Boolean
+    ) {
+        val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
+        val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
+
+        // Swap the margins that are set in TaskView#setRecentsOrientedState()
+        secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx
+        primaryParams.topMargin = 0
+
+        // Measure and layout the thumbnails bottom up, since the primary is on the visual left
+        // (portrait bottom) and secondary is on the right (portrait top)
+        val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
+        val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
+        val dividerBar =
+            Math.round(
+                totalThumbnailHeight *
+                    if (splitBoundsConfig.appsStackedVertically)
+                        splitBoundsConfig.dividerHeightPercent
+                    else splitBoundsConfig.dividerWidthPercent
+            )
+        val (taskViewFirst, taskViewSecond) =
+            getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
+        secondarySnapshot.translationY = 0f
+        primarySnapshot.translationY =
+            (taskViewSecond.y + spaceAboveSnapshot + dividerBar).toFloat()
+        primarySnapshot.measure(
+            MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY),
+            MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY)
+        )
+        secondarySnapshot.measure(
+            MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY),
+            MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY)
+        )
+    }
+
+    override fun getGroupedTaskViewSizes(
+        dp: DeviceProfile,
+        splitBoundsConfig: SplitBounds,
+        parentWidth: Int,
+        parentHeight: Int
+    ): Pair<Point, Point> {
+        // Measure and layout the thumbnails bottom up, since the primary is on the visual left
+        // (portrait bottom) and secondary is on the right (portrait top)
+        val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
+        val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
+        val dividerBar =
+            Math.round(
+                totalThumbnailHeight *
+                    if (splitBoundsConfig.appsStackedVertically)
+                        splitBoundsConfig.dividerHeightPercent
+                    else splitBoundsConfig.dividerWidthPercent
+            )
+        val taskPercent =
+            if (splitBoundsConfig.appsStackedVertically) {
+                splitBoundsConfig.topTaskPercent
+            } else {
+                splitBoundsConfig.leftTaskPercent
+            }
+        val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
+        val secondTaskViewSize =
+            Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
+        return Pair(firstTaskViewSize, secondTaskViewSize)
+    }
+
+    /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
+    override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction =
+        SingleAxisSwipeDetector.HORIZONTAL
+
+    override fun getUpDirection(isRtl: Boolean): Int =
+        if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE
+        else SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+
+    override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
+        if (isRtl) displacement > 0 else displacement < 0
+
+    override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
+    /* -------------------- */
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 133749d..2575e61 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -5031,7 +5031,7 @@
         firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
 
         RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
-        Pair<FloatProperty, FloatProperty> taskViewsFloat =
+        Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
                 orientationHandler.getSplitSelectTaskOffset(
                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
                         mActivity.getDeviceProfile());