Merge "Adding coroutines library" into main
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index 39fb158..1640104 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.orientation
 
+import android.annotation.SuppressLint
 import android.content.res.Resources
 import android.graphics.Point
 import android.graphics.PointF
@@ -33,6 +34,7 @@
 import android.view.accessibility.AccessibilityEvent
 import android.widget.FrameLayout
 import android.widget.LinearLayout
+import androidx.annotation.VisibleForTesting
 import androidx.core.util.component1
 import androidx.core.util.component2
 import com.android.launcher3.DeviceProfile
@@ -44,7 +46,11 @@
 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.STAGE_POSITION_BOTTOM_OR_RIGHT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN
+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
@@ -451,13 +457,8 @@
         // (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 dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
         val (taskViewFirst, taskViewSecond) =
             getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
 
@@ -482,13 +483,8 @@
     ): 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 dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
         val taskPercent =
             if (splitBoundsConfig.appsStackedVertically) {
                 splitBoundsConfig.topTaskPercent
@@ -569,64 +565,22 @@
         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)
+        val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx
+        val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot
+        val dividerBar: Int = getDividerBarSize(totalThumbnailHeight, splitConfig)
 
-        // 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
+        val (topLeftY, bottomRightY) =
+            getSplitIconsPosition(
+                taskIconHeight,
+                primarySnapshotHeight,
+                totalThumbnailHeight,
+                isRtl,
+                deviceProfile.overviewTaskMarginPx,
+                dividerBar
+            )
+
+        updateSplitIconsPosition(primaryIconView, topLeftY, isRtl)
+        updateSplitIconsPosition(secondaryIconView, bottomRightY, isRtl)
     }
 
     override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int {
@@ -656,4 +610,91 @@
 
     override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float =
         floatingTask.translationY
+
+    /**
+     * Retrieves split icons position
+     *
+     * @param taskIconHeight The height of the task icon.
+     * @param primarySnapshotHeight The height for the primary snapshot (i.e., top-left snapshot).
+     * @param totalThumbnailHeight The total height for the group task view.
+     * @param isRtl Whether the layout direction is RTL (or false for LTR).
+     * @param overviewTaskMarginPx The space under the focused task icon provided by Device Profile.
+     * @param dividerSize The size of the divider for the group task view.
+     * @return The top-left and right-bottom positions for the icon views.
+     */
+    @VisibleForTesting
+    open fun getSplitIconsPosition(
+        taskIconHeight: Int,
+        primarySnapshotHeight: Int,
+        totalThumbnailHeight: Int,
+        isRtl: Boolean,
+        overviewTaskMarginPx: Int,
+        dividerSize: Int,
+    ): SplitIconPositions {
+        return if (Flags.enableOverviewIconMenu()) {
+            if (isRtl) {
+                SplitIconPositions(0, -(totalThumbnailHeight - primarySnapshotHeight))
+            } else {
+                SplitIconPositions(0, primarySnapshotHeight + dividerSize)
+            }
+        } else {
+            val topLeftY = primarySnapshotHeight + overviewTaskMarginPx
+            SplitIconPositions(
+                topLeftY = topLeftY,
+                bottomRightY = topLeftY + dividerSize + taskIconHeight
+            )
+        }
+    }
+
+    /**
+     * Updates icon view gravity and translation for split tasks
+     *
+     * @param iconView View to be updated
+     * @param translationY the translationY that should be applied
+     * @param isRtl Whether the layout direction is RTL (or false for LTR).
+     */
+    @SuppressLint("RtlHardcoded")
+    @VisibleForTesting
+    open fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) {
+        val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams
+
+        if (Flags.enableOverviewIconMenu()) {
+            val appChipView = iconView as IconAppChipView
+            layoutParams.gravity =
+                if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END
+            appChipView.layoutParams = layoutParams
+            appChipView.setSplitTranslationX(0f)
+            appChipView.setSplitTranslationY(translationY.toFloat())
+        } else {
+            layoutParams.gravity = Gravity.TOP or Gravity.RIGHT
+            layoutParams.topMargin = translationY
+            iconView.translationX = 0f
+            iconView.translationY = 0f
+            iconView.layoutParams = layoutParams
+        }
+    }
+
+    /**
+     * It calculates the divider's size in the group task view.
+     *
+     * @param totalThumbnailHeight The total height for the group task view
+     * @param splitConfig Contains information about sizes and proportions for split task.
+     * @return The divider size for the group task view.
+     */
+    protected fun getDividerBarSize(totalThumbnailHeight: Int, splitConfig: SplitBounds): Int {
+        return Math.round(
+            totalThumbnailHeight *
+                if (splitConfig.appsStackedVertically) splitConfig.dividerHeightPercent
+                else splitConfig.dividerWidthPercent
+        )
+    }
+
+    /**
+     * Data structure to keep the y position to be used for the split task icon views translation.
+     *
+     * @param topLeftY The y-axis position for the task view position on the Top or Left side.
+     * @param bottomRightY The y-axis position for the task view position on the Bottom or Right
+     *   side.
+     */
+    data class SplitIconPositions(val topLeftY: Int, val bottomRightY: Int)
 }
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 62dfd82..0476fe8 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -75,6 +75,7 @@
     public <T> T getSecondaryValue(T x, T y) {
         return y;
     }
+
     @Override
     public boolean isLayoutNaturalToLauncher() {
         return true;
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index 0c78b8f..5bebf8c 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.orientation
 
+import android.annotation.SuppressLint
 import android.content.res.Resources
 import android.graphics.Point
 import android.graphics.PointF
@@ -32,7 +33,12 @@
 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.STAGE_POSITION_BOTTOM_OR_RIGHT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN
+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
 
@@ -255,97 +261,6 @@
         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,
@@ -366,13 +281,8 @@
         // (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 dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
         val (taskViewFirst, taskViewSecond) =
             getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
         secondarySnapshot.translationY = 0f
@@ -398,13 +308,8 @@
         // (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 dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
         val taskPercent =
             if (splitBoundsConfig.appsStackedVertically) {
                 splitBoundsConfig.topTaskPercent
@@ -430,4 +335,64 @@
 
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
     /* -------------------- */
+
+    override fun getSplitIconsPosition(
+        taskIconHeight: Int,
+        primarySnapshotHeight: Int,
+        totalThumbnailHeight: Int,
+        isRtl: Boolean,
+        overviewTaskMarginPx: Int,
+        dividerSize: Int,
+    ): SplitIconPositions {
+        return if (Flags.enableOverviewIconMenu()) {
+            if (isRtl) {
+                SplitIconPositions(
+                    topLeftY = totalThumbnailHeight - primarySnapshotHeight,
+                    bottomRightY = 0
+                )
+            } else {
+                SplitIconPositions(
+                    topLeftY = 0,
+                    bottomRightY = -(primarySnapshotHeight + dividerSize)
+                )
+            }
+        } else {
+            // In seascape, the icons are initially placed at the bottom start of the
+            // display (portrait locked). The values defined here are used to translate the icons
+            // from the bottom to the almost-center of the screen using the bottom margin.
+            // The primary snapshot is placed at the bottom, thus we translate the icons using
+            // the size of the primary snapshot minus the icon size for the top-left icon.
+            SplitIconPositions(
+                topLeftY = primarySnapshotHeight - taskIconHeight,
+                bottomRightY = primarySnapshotHeight + dividerSize
+            )
+        }
+    }
+
+    /**
+     * Updates icon view gravity and translation for split tasks
+     *
+     * @param iconView View to be updated
+     * @param translationY the translationY that should be applied
+     * @param isRtl Whether the layout direction is RTL (or false for LTR).
+     */
+    @SuppressLint("RtlHardcoded")
+    override fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) {
+        val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams
+
+        if (Flags.enableOverviewIconMenu()) {
+            val appChipView = iconView as IconAppChipView
+            layoutParams.gravity =
+                if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START
+            appChipView.layoutParams = layoutParams
+            appChipView.setSplitTranslationX(0f)
+            appChipView.setSplitTranslationY(translationY.toFloat())
+        } else {
+            layoutParams.gravity = Gravity.BOTTOM or Gravity.LEFT
+            iconView.translationX = 0f
+            iconView.translationY = 0f
+            layoutParams.bottomMargin = translationY
+            iconView.layoutParams = layoutParams
+        }
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt b/quickstep/tests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt
new file mode 100644
index 0000000..ea52842
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.platform.test.flag.junit.SetFlagsRule
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
+import com.android.quickstep.orientation.LandscapePagedViewHandler.SplitIconPositions
+import com.android.quickstep.views.IconAppChipView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+class LandscapePagedViewHandlerTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val sut = LandscapePagedViewHandler()
+
+    private fun enableGridOnlyOverview(isEnabled: Boolean) {
+        if (isEnabled) {
+            setFlagsRule.enableFlags(
+                Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+                Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+            )
+        } else {
+            setFlagsRule.disableFlags(
+                Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+                Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+            )
+        }
+    }
+
+    /** [ Test getSplitIconsPosition ] */
+    private fun getSplitIconsPosition(isRTL: Boolean): SplitIconPositions {
+        return sut.getSplitIconsPosition(
+            TASK_ICON_HEIGHT_PX,
+            PRIMARY_SNAPSHOT,
+            TOTAL_THUMBNAIL_HEIGHT,
+            isRTL,
+            OVERVIEW_TASK_MARGIN_PX,
+            DIVIDER_SIZE_PX,
+        )
+    }
+
+    @Test
+    fun testIcon_getSplitIconsPositions() {
+        enableGridOnlyOverview(false)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+        // Top-Left icon should be at the end of the primary snapshot height
+        assertThat(topLeftY).isEqualTo(250)
+        // Bottom-Right icon should be at the end of the primary height + divider + icon size
+        assertThat(bottomRightY).isEqualTo(374)
+    }
+
+    @Test
+    fun testIcon_getSplitIconsPositions_isRTL() {
+        enableGridOnlyOverview(false)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+        // Top-Left icon should be at the end of the primary snapshot height
+        assertThat(topLeftY).isEqualTo(250)
+        // Bottom-Right icon should be at the end of the primary height + divider + icon size
+        assertThat(bottomRightY).isEqualTo(374)
+    }
+
+    @Test
+    fun testChip_getSplitIconsPositions() {
+        enableGridOnlyOverview(true)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+        // Top-Left app chip should always be at the initial position of the first snapshot
+        assertThat(topLeftY).isEqualTo(0)
+        // Bottom-Right app chip should be at the end of the primary height + divider
+        assertThat(bottomRightY).isEqualTo(266)
+    }
+
+    @Test
+    fun testChip_getSplitIconsPositions_isRTL() {
+        enableGridOnlyOverview(true)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+        // TODO(b/326377497): When started in fake seascape and rotated to landscape,
+        //  the icon chips are in RTL and wrongly positioned at the right side of the snapshot.
+        //  Top-Left app chip should be placed at the top left of the first snapshot, but because
+        //  this issue, it's displayed at the top-right of the second snapshot.
+        //  The Bottom-Right app chip is displayed at the top-right of the first snapshot because
+        //  of this issue.
+        assertThat(topLeftY).isEqualTo(0)
+        assertThat(bottomRightY).isEqualTo(-316)
+    }
+
+    /** Test updateSplitIconsPosition */
+    @Test
+    fun testIcon_updateSplitIconsPosition() {
+        enableGridOnlyOverview(false)
+
+        val expectedTranslationY = 250
+        val expectedGravity = Gravity.TOP or Gravity.RIGHT
+
+        val iconView = mock<View>()
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+        assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+        assertThat(frameLayout.topMargin).isEqualTo(expectedTranslationY)
+        verify(iconView).translationX = 0f
+        verify(iconView).translationY = 0f
+    }
+
+    @Test
+    fun testIcon_updateSplitIconsPosition_isRTL() {
+        enableGridOnlyOverview(false)
+
+        val expectedTranslationY = 250
+        val expectedGravity = Gravity.TOP or Gravity.RIGHT
+
+        val iconView = mock<View>()
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+        assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+        assertThat(frameLayout.topMargin).isEqualTo(expectedTranslationY)
+        verify(iconView).translationX = 0f
+        verify(iconView).translationY = 0f
+    }
+
+    @Test
+    fun testChip_updateSplitIconsPosition() {
+        enableGridOnlyOverview(true)
+
+        val expectedTranslationY = 250
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        val iconView = mock<IconAppChipView>()
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+        assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END)
+        verify(iconView).setSplitTranslationX(0f)
+        verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+    }
+
+    @Test
+    fun testChip_updateSplitIconsPosition_isRTL() {
+        enableGridOnlyOverview(true)
+
+        val expectedTranslationY = 250
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        val iconView = mock<IconAppChipView>()
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+        assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START)
+        verify(iconView).setSplitTranslationX(0f)
+        verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+    }
+
+    private companion object {
+        const val TASK_ICON_HEIGHT_PX = 108
+        const val OVERVIEW_TASK_MARGIN_PX = 0
+        const val DIVIDER_SIZE_PX = 16
+        const val PRIMARY_SNAPSHOT = 250
+        const val SECONDARY_SNAPSHOT = 300
+        const val TOTAL_THUMBNAIL_HEIGHT = PRIMARY_SNAPSHOT + SECONDARY_SNAPSHOT + DIVIDER_SIZE_PX
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt b/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt
new file mode 100644
index 0000000..2bc182c
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.platform.test.flag.junit.SetFlagsRule
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
+import com.android.quickstep.orientation.LandscapePagedViewHandler.SplitIconPositions
+import com.android.quickstep.views.IconAppChipView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+class SeascapePagedViewHandlerTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val sut = SeascapePagedViewHandler()
+
+    private fun enableGridOnlyOverview(isEnabled: Boolean) {
+        if (isEnabled) {
+            setFlagsRule.enableFlags(
+                Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+                Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+            )
+        } else {
+            setFlagsRule.disableFlags(
+                Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+                Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+            )
+        }
+    }
+
+    /** [ Test getSplitIconsPosition ] */
+    private fun getSplitIconsPosition(isRTL: Boolean): SplitIconPositions {
+        return sut.getSplitIconsPosition(
+            TASK_ICON_HEIGHT_PX,
+            PRIMARY_SNAPSHOT,
+            TOTAL_THUMBNAIL_HEIGHT,
+            isRTL,
+            OVERVIEW_TASK_MARGIN_PX,
+            DIVIDER_SIZE_PX,
+        )
+    }
+
+    @Test
+    fun testIcon_getSplitIconsPositions() {
+        enableGridOnlyOverview(false)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+        // The top-left icon is translated from the bottom of the screen to the end of
+        // the primary snapshot minus the icon size.
+        assertThat(topLeftY).isEqualTo(142)
+        // The bottom-right icon is placed at the end of the primary snapshot plus the divider.
+        assertThat(bottomRightY).isEqualTo(266)
+    }
+
+    @Test
+    fun testIcon_getSplitIconsPositions_isRTL() {
+        enableGridOnlyOverview(false)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+        // The top-left icon is translated from the bottom of the screen to the end of
+        // the primary snapshot minus the icon size.
+        assertThat(topLeftY).isEqualTo(142)
+        // The bottom-right icon is placed at the end of the primary snapshot plus the divider.
+        assertThat(bottomRightY).isEqualTo(266)
+    }
+
+    @Test
+    fun testChip_getSplitIconsPositions() {
+        enableGridOnlyOverview(true)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+        // Top-Left app chip should always be at the initial position of the first snapshot
+        assertThat(topLeftY).isEqualTo(0)
+        // Bottom-Right app chip should be at the end of the primary height + divider
+        assertThat(bottomRightY).isEqualTo(-266)
+    }
+
+    @Test
+    fun testChip_getSplitIconsPositions_isRTL() {
+        enableGridOnlyOverview(true)
+
+        val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+        // TODO(b/326377497): When started in fake seascape and rotated to landscape,
+        //  the icon chips are in RTL and wrongly positioned at the right side of the snapshot.
+        //  Top-Left app chip should be placed at the top left of the first snapshot, but because
+        //  this issue, it's displayed at the top-right of the second snapshot.
+        //  The Bottom-Right app chip is displayed at the top-right of the first snapshot because
+        //  of this issue.
+        assertThat(topLeftY).isEqualTo(316)
+        assertThat(bottomRightY).isEqualTo(0)
+    }
+
+    /** Test updateSplitIconsPosition */
+    @Test
+    fun testIcon_updateSplitIconsPosition() {
+        enableGridOnlyOverview(false)
+
+        val expectedTranslationY = 250
+        val expectedGravity = Gravity.BOTTOM or Gravity.LEFT
+
+        val iconView = mock<View>()
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+        assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+        assertThat(frameLayout.bottomMargin).isEqualTo(expectedTranslationY)
+        verify(iconView).translationX = 0f
+        verify(iconView).translationY = 0f
+    }
+
+    @Test
+    fun testIcon_updateSplitIconsPosition_isRTL() {
+        enableGridOnlyOverview(false)
+
+        val expectedTranslationY = 250
+        val expectedGravity = Gravity.BOTTOM or Gravity.LEFT
+
+        val iconView = mock<View>()
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+        assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+        assertThat(frameLayout.bottomMargin).isEqualTo(expectedTranslationY)
+        verify(iconView).translationX = 0f
+        verify(iconView).translationY = 0f
+    }
+
+    @Test
+    fun testChip_updateSplitIconsPosition() {
+        enableGridOnlyOverview(true)
+
+        val expectedTranslationY = 250
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        val iconView = mock<IconAppChipView>()
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+        assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START)
+        verify(iconView).setSplitTranslationX(0f)
+        verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+    }
+
+    @Test
+    fun testChip_updateSplitIconsPosition_isRTL() {
+        enableGridOnlyOverview(true)
+
+        val expectedTranslationY = 250
+        val frameLayout = FrameLayout.LayoutParams(100, 100)
+        val iconView = mock<IconAppChipView>()
+        `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+        sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+        assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END)
+        verify(iconView).setSplitTranslationX(0f)
+        verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+    }
+
+    private companion object {
+        const val TASK_ICON_HEIGHT_PX = 108
+        const val OVERVIEW_TASK_MARGIN_PX = 0
+        const val DIVIDER_SIZE_PX = 16
+        const val PRIMARY_SNAPSHOT = 250
+        const val SECONDARY_SNAPSHOT = 300
+        const val TOTAL_THUMBNAIL_HEIGHT = PRIMARY_SNAPSHOT + SECONDARY_SNAPSHOT + DIVIDER_SIZE_PX
+    }
+}