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
+ }
+}