diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index da1de8dc16..61c5f40dc2 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -92,6 +92,14 @@
8dp
18dp
16dp
+
+ 24dp
+ 24dp
+ 72dp
+ 72dp
+ 72dp
+ 24dp
+ 24dp
25dp
diff --git a/quickstep/src/com/android/quickstep/recents/domain/model/DesktopLayoutConfig.kt b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopLayoutConfig.kt
new file mode 100644
index 0000000000..8c2d79519e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopLayoutConfig.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2025 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.recents.domain.model
+
+/**
+ * Holds pre-scaled configuration values related to desktop task layout dimensions. These values are
+ * typically derived from resources and then scaled according to the current view and screen
+ * dimensions.
+ *
+ * @property topBottomMarginOneRow Scaled margin for top/bottom when one row is shown.
+ * @property topMarginMultiRows Scaled top margin when multiple rows are shown.
+ * @property bottomMarginMultiRows Scaled bottom margin when multiple rows are shown.
+ * @property leftRightMarginOneRow Scaled margin for left/right when one row is shown.
+ * @property leftRightMarginMultiRows Scaled margin for left/right when multiple rows are shown.
+ * @property horizontalPaddingBetweenTasks Scaled horizontal padding between tasks.
+ * @property verticalPaddingBetweenTasks Scaled vertical padding between tasks.
+ */
+data class DesktopLayoutConfig(
+ val topBottomMarginOneRow: Int,
+ val topMarginMultiRows: Int,
+ val bottomMarginMultiRows: Int,
+ val leftRightMarginOneRow: Int,
+ val leftRightMarginMultiRows: Int,
+ val horizontalPaddingBetweenTasks: Int,
+ val verticalPaddingBetweenTasks: Int,
+)
diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt
index 4ea39d816e..869677149f 100644
--- a/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt
@@ -19,6 +19,7 @@ package com.android.quickstep.recents.domain.usecase
import android.graphics.Rect
import android.graphics.RectF
import androidx.core.graphics.toRect
+import com.android.quickstep.recents.domain.model.DesktopLayoutConfig
import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
/** This usecase is responsible for organizing desktop windows in a non-overlapping way. */
@@ -33,9 +34,10 @@ class OrganizeDesktopTasksUseCase {
* row height which is equivalent assuming fixed height), balanced rows and minimal wasted
* space.
*/
- fun run(
+ operator fun invoke(
desktopBounds: Rect,
taskBounds: List,
+ layoutConfig: DesktopLayoutConfig,
): List {
if (desktopBounds.isEmpty || taskBounds.isEmpty()) {
return emptyList()
@@ -48,13 +50,43 @@ class OrganizeDesktopTasksUseCase {
return emptyList()
}
- val availableLayoutBounds = desktopBounds.getLayoutEffectiveBounds()
- val resultRects = findOptimalHeightAndBalancedWidth(availableLayoutBounds, validTaskBounds)
+ // Assuming we can place all windows in one row, do one pass first to check whether all
+ // windows can fit.
+ var availableLayoutBounds =
+ desktopBounds.getLayoutEffectiveBounds(
+ singleRow = true,
+ taskNumber = taskBounds.size,
+ layoutConfig,
+ )
+ var resultRects =
+ findOptimalHeightAndBalancedWidth(
+ availableLayoutBounds,
+ validTaskBounds,
+ layoutConfig,
+ singleRow = true,
+ )
+
+ if (!canFitInOneRow(resultRects)) {
+ availableLayoutBounds =
+ desktopBounds.getLayoutEffectiveBounds(
+ singleRow = true,
+ taskNumber = taskBounds.size,
+ layoutConfig,
+ )
+ resultRects =
+ findOptimalHeightAndBalancedWidth(
+ availableLayoutBounds,
+ validTaskBounds,
+ layoutConfig,
+ singleRow = true,
+ )
+ }
centerTaskWindows(
availableLayoutBounds,
resultRects.maxOf { it.bottom }.toInt(),
resultRects,
+ layoutConfig,
)
val result = mutableListOf()
@@ -65,8 +97,39 @@ class OrganizeDesktopTasksUseCase {
}
/** Calculates the effective bounds for layout by applying insets to the raw desktop bounds. */
- private fun Rect.getLayoutEffectiveBounds() =
- Rect(this).apply { inset(OVERVIEW_INSET_TOP_BOTTOM, OVERVIEW_INSET_LEFT_RIGHT) }
+ private fun Rect.getLayoutEffectiveBounds(
+ singleRow: Boolean,
+ taskNumber: Int,
+ layoutConfig: DesktopLayoutConfig,
+ ) =
+ Rect(this).apply {
+ val topInset =
+ if (singleRow) layoutConfig.topBottomMarginOneRow
+ else layoutConfig.topMarginMultiRows
+ val bottomInset =
+ if (singleRow) layoutConfig.topBottomMarginOneRow
+ else layoutConfig.bottomMarginMultiRows
+ val leftInset =
+ if (singleRow && taskNumber <= 1) layoutConfig.leftRightMarginOneRow
+ else layoutConfig.leftRightMarginMultiRows
+ val rightInset =
+ if (singleRow && taskNumber <= 1) leftInset
+ else (leftInset - layoutConfig.horizontalPaddingBetweenTasks)
+
+ inset(leftInset, topInset, rightInset, bottomInset)
+ }
+
+ /** Calculates the maximum height for a task window in the desktop tile in Overview. */
+ private fun getMaxTaskHeight(
+ effectiveLayoutBounds: Rect,
+ layoutConfig: DesktopLayoutConfig,
+ singleRow: Boolean,
+ ) =
+ if (singleRow) {
+ effectiveLayoutBounds.height()
+ } else {
+ effectiveLayoutBounds.height() - 2 * layoutConfig.verticalPaddingBetweenTasks
+ }
/**
* Determines the optimal height for task windows and balances the row widths to minimize wasted
@@ -75,6 +138,8 @@ class OrganizeDesktopTasksUseCase {
private fun findOptimalHeightAndBalancedWidth(
availableLayoutBounds: Rect,
validTaskBounds: List,
+ layoutConfig: DesktopLayoutConfig,
+ singleRow: Boolean,
): List {
// Right bound of the narrowest row.
var minRight: Int
@@ -92,7 +157,7 @@ class OrganizeDesktopTasksUseCase {
// Determine the optimal height bisecting between [lowHeight] and [highHeight]. Once this
// optimal height is known, [heightFixed] is set to `true` and the rows are balanced by
// repeatedly squeezing the widest row to cause windows to overflow to the subsequent rows.
- var lowHeight = VERTICAL_SPACE_BETWEEN_TASKS
+ var lowHeight = layoutConfig.verticalPaddingBetweenTasks
var highHeight = maxOf(lowHeight, availableLayoutBounds.height() + 1)
var optimalHeight = 0.5f * (lowHeight + highHeight)
var heightFixed = false
@@ -113,7 +178,11 @@ class OrganizeDesktopTasksUseCase {
fitWindowRectsInBounds(
Rect(availableLayoutBounds).apply { right = rightBound },
validTaskBounds,
- minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()),
+ minOf(
+ getMaxTaskHeight(availableLayoutBounds, layoutConfig, singleRow),
+ optimalHeight.toInt(),
+ ),
+ layoutConfig,
)
val allWindowsFit = fitWindowResult.allWindowsFit
resultRects = fitWindowResult.calculatedBounds
@@ -168,7 +237,11 @@ class OrganizeDesktopTasksUseCase {
fitWindowRectsInBounds(
Rect(availableLayoutBounds).apply { right = rightBound },
validTaskBounds,
- minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()),
+ minOf(
+ getMaxTaskHeight(availableLayoutBounds, layoutConfig, singleRow),
+ optimalHeight.toInt(),
+ ),
+ layoutConfig,
)
resultRects = fitWindowResult.calculatedBounds
}
@@ -199,10 +272,14 @@ class OrganizeDesktopTasksUseCase {
layoutBounds: Rect,
taskBounds: List,
optimalWindowHeight: Int,
+ layoutConfig: DesktopLayoutConfig,
): FitWindowResult {
val numTasks = taskBounds.size
val outRects = mutableListOf()
+ val verticalPadding = layoutConfig.verticalPaddingBetweenTasks
+ val horizontalPadding = layoutConfig.horizontalPaddingBetweenTasks
+
// Start in the top-left corner of [layoutBounds].
var left = layoutBounds.left
var top = layoutBounds.top
@@ -219,9 +296,9 @@ class OrganizeDesktopTasksUseCase {
// Use the height to calculate the width
val scale = optimalWindowHeight / taskBounds.height().toFloat()
val width = (taskBounds.width() * scale).toInt()
- val optimalRowHeight = optimalWindowHeight + VERTICAL_SPACE_BETWEEN_TASKS
+ val optimalRowHeight = optimalWindowHeight + verticalPadding
- if ((left + width + HORIZONTAL_SPACE_BETWEEN_TASKS) > layoutBounds.right) {
+ if (left + width + horizontalPadding > layoutBounds.right) {
// Move to the next row if possible.
minRight = minOf(minRight, left)
maxRight = maxOf(maxRight, left)
@@ -231,8 +308,7 @@ class OrganizeDesktopTasksUseCase {
// row does not fit within the available width.
if (
(top + optimalRowHeight) > layoutBounds.bottom ||
- layoutBounds.left + width + HORIZONTAL_SPACE_BETWEEN_TASKS >
- layoutBounds.right
+ layoutBounds.left + width + horizontalPadding > layoutBounds.right
) {
allWindowsFit = false
break
@@ -251,7 +327,7 @@ class OrganizeDesktopTasksUseCase {
)
// Increment horizontal position.
- left += (width + HORIZONTAL_SPACE_BETWEEN_TASKS)
+ left += (width + horizontalPadding)
}
// Update the narrowest and widest row width for the last row.
@@ -262,7 +338,12 @@ class OrganizeDesktopTasksUseCase {
}
/** Centers task windows in the center of Overview. */
- private fun centerTaskWindows(layoutBounds: Rect, maxBottom: Int, outWindowRects: List) {
+ private fun centerTaskWindows(
+ layoutBounds: Rect,
+ maxBottom: Int,
+ outWindowRects: List,
+ layoutConfig: DesktopLayoutConfig,
+ ) {
if (outWindowRects.isEmpty()) {
return
}
@@ -271,11 +352,14 @@ class OrganizeDesktopTasksUseCase {
var currentRowY = outWindowRects[0].top
var currentRowFirstItemIndex = 0
val offsetY = (layoutBounds.bottom - maxBottom) / 2f
+ val horizontal_padding =
+ if (outWindowRects.size == 1) 0 else layoutConfig.horizontalPaddingBetweenTasks
// Batch process to center overview desktop task windows within the same row.
fun batchCenterDesktopTaskWindows(endIndex: Int) {
// Calculate the shift amount required to center the desktop task items.
- val rangeCenterX = (currentRowUnionRange.left + currentRowUnionRange.right) / 2f
+ val rangeCenterX =
+ (currentRowUnionRange.left + currentRowUnionRange.right + horizontal_padding) / 2f
val currentDiffX = (layoutBounds.centerX() - rangeCenterX).coerceAtLeast(0f)
for (j in currentRowFirstItemIndex until endIndex) {
outWindowRects[j].offset(currentDiffX, offsetY)
@@ -301,11 +385,13 @@ class OrganizeDesktopTasksUseCase {
batchCenterDesktopTaskWindows(outWindowRects.size)
}
- private companion object {
- const val VERTICAL_SPACE_BETWEEN_TASKS = 24
- const val HORIZONTAL_SPACE_BETWEEN_TASKS = 24
- const val OVERVIEW_INSET_TOP_BOTTOM = 16
- const val OVERVIEW_INSET_LEFT_RIGHT = 16
- const val MAXIMUM_TASK_HEIGHT = 800
+ /** Returns true if all task windows can fit in one row. */
+ private fun canFitInOneRow(resultRect: List): Boolean {
+ if (resultRect.isEmpty()) {
+ return true
+ }
+
+ val firstTop = resultRect.first().top
+ return resultRect.all { it.top == firstTop }
}
}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt
index 4de0b90045..4e0e9609d0 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt
@@ -18,6 +18,7 @@ package com.android.quickstep.recents.ui.viewmodel
import android.graphics.Rect
import android.util.Size
+import com.android.quickstep.recents.domain.model.DesktopLayoutConfig
import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
@@ -34,12 +35,18 @@ class DesktopTaskViewModel(private val organizeDesktopTasksUseCase: OrganizeDesk
*
* @param desktopSize the size available for organizing the tasks.
* @param defaultPositions the tasks and their bounds as they appear on a desktop.
+ * @param layoutConfig the pre-scaled dimension configuration for the desktop layout.
*/
- fun organizeDesktopTasks(desktopSize: Size, defaultPositions: List) {
+ fun organizeDesktopTasks(
+ desktopSize: Size,
+ defaultPositions: List,
+ layoutConfig: DesktopLayoutConfig,
+ ) {
organizedDesktopTaskPositions =
- organizeDesktopTasksUseCase.run(
+ organizeDesktopTasksUseCase(
desktopBounds = Rect(0, 0, desktopSize.width, desktopSize.height),
taskBounds = defaultPositions,
+ layoutConfig = layoutConfig,
)
}
}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 7aa24a9d60..d471b005c7 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -59,6 +59,7 @@ import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.ViewUtils
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
+import com.android.quickstep.recents.domain.model.DesktopLayoutConfig
import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel
import com.android.quickstep.recents.ui.viewmodel.TaskData
@@ -209,19 +210,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
return
}
- val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
-
- val taskViewWidth = layoutParams.width
- val taskViewHeight = layoutParams.height - thumbnailTopMarginPx
-
- BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
-
- val screenWidth = tempPointF.x.toInt()
- val screenHeight = tempPointF.y.toInt()
- val screenRect = Rect(0, 0, screenWidth, screenHeight)
- val scaleWidth = taskViewWidth / screenWidth.toFloat()
- val scaleHeight = taskViewHeight / screenHeight.toFloat()
-
+ val (widthScale, heightScale) = getScreenScaleFactors()
taskContainers.forEach { taskContainer ->
val taskId = taskContainer.task.key.id
val fullscreenTaskBounds =
@@ -264,7 +253,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
val fromRect =
TEMP_FROM_RECTF.apply {
set(fullscreenTaskBounds)
- scale(scaleWidth)
+ scale(widthScale)
offset(
lastComputedTaskSize.left.toFloat(),
lastComputedTaskSize.top.toFloat(),
@@ -273,7 +262,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
val toRect =
TEMP_TO_RECTF.apply {
set(currentTaskBounds)
- scale(scaleWidth)
+ scale(widthScale)
offset(
lastComputedTaskSize.left.toFloat(),
lastComputedTaskSize.top.toFloat(),
@@ -286,10 +275,11 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
}
}
- val overviewTaskLeft = overviewTaskBounds.left * scaleWidth
- val overviewTaskTop = overviewTaskBounds.top * scaleHeight
- val overviewTaskWidth = overviewTaskBounds.width() * scaleWidth
- val overviewTaskHeight = overviewTaskBounds.height() * scaleHeight
+ val overviewTaskLeft = overviewTaskBounds.left * widthScale
+ val overviewTaskTop = overviewTaskBounds.top * heightScale
+ val overviewTaskWidth = overviewTaskBounds.width() * widthScale
+ val overviewTaskHeight = overviewTaskBounds.height() * heightScale
+
if (updateLayout) {
// Position the task to the same position as it would be on the desktop
taskContainer.taskContentView.updateLayoutParams {
@@ -308,6 +298,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
overviewTaskBounds.width().toFloat() / currentTaskBounds.width()
val thumbnailScaleHeight =
overviewTaskBounds.height().toFloat() / currentTaskBounds.height()
+ val screenRect = getScreenRect()
val contentOutlineBounds =
if (intersects(currentTaskBounds, screenRect))
Rect(currentTaskBounds).apply {
@@ -315,10 +306,10 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
// Offset to 0,0 to transform into TaskThumbnailView's coordinate
// system.
offset(-currentTaskBounds.left, -currentTaskBounds.top)
- left = (left * scaleWidth * thumbnailScaleWidth).roundToInt()
- top = (top * scaleHeight * thumbnailScaleHeight).roundToInt()
- right = (right * scaleWidth * thumbnailScaleWidth).roundToInt()
- bottom = (bottom * scaleHeight * thumbnailScaleHeight).roundToInt()
+ left = (left * widthScale * thumbnailScaleWidth).roundToInt()
+ top = (top * heightScale * thumbnailScaleHeight).roundToInt()
+ right = (right * widthScale * thumbnailScaleWidth).roundToInt()
+ bottom = (bottom * heightScale * thumbnailScaleHeight).roundToInt()
}
else null
@@ -330,10 +321,10 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
}
}
- val currentTaskLeft = currentTaskBounds.left * scaleWidth
- val currentTaskTop = currentTaskBounds.top * scaleHeight
- val currentTaskWidth = currentTaskBounds.width() * scaleWidth
- val currentTaskHeight = currentTaskBounds.height() * scaleHeight
+ val currentTaskLeft = currentTaskBounds.left * widthScale
+ val currentTaskTop = currentTaskBounds.top * heightScale
+ val currentTaskWidth = currentTaskBounds.width() * widthScale
+ val currentTaskHeight = currentTaskBounds.height() * heightScale
// During the animation, apply translation and scale such that the view is transformed
// to where we want, without triggering layout.
taskContainer.taskContentView.apply {
@@ -341,8 +332,9 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
pivotY = 0.0f
translationX = currentTaskLeft - overviewTaskLeft
translationY = currentTaskTop - overviewTaskTop
- scaleX = currentTaskWidth / overviewTaskWidth
- scaleY = currentTaskHeight / overviewTaskHeight
+ scaleX = if (overviewTaskWidth != 0f) currentTaskWidth / overviewTaskWidth else 1f
+ scaleY =
+ if (overviewTaskHeight != 0f) currentTaskHeight / overviewTaskHeight else 1f
}
if (taskContainer.task.isMinimized) {
@@ -647,11 +639,72 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
}
if (enableDesktopExplodedView()) {
- viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions)
+ val (widthScale, heightScale) = getScreenScaleFactors()
+ val res = context.resources
+ val layoutConfig =
+ DesktopLayoutConfig(
+ topBottomMarginOneRow =
+ (res.getDimensionPixelSize(R.dimen.desktop_top_bottom_margin_one_row) /
+ heightScale)
+ .toInt(),
+ topMarginMultiRows =
+ (res.getDimensionPixelSize(R.dimen.desktop_top_margin_multi_rows) /
+ heightScale)
+ .toInt(),
+ bottomMarginMultiRows =
+ (res.getDimensionPixelSize(R.dimen.desktop_bottom_margin_multi_rows) /
+ heightScale)
+ .toInt(),
+ leftRightMarginOneRow =
+ (res.getDimensionPixelSize(R.dimen.desktop_left_right_margin_one_row) /
+ widthScale)
+ .toInt(),
+ leftRightMarginMultiRows =
+ (res.getDimensionPixelSize(R.dimen.desktop_left_right_margin_multi_rows) /
+ widthScale)
+ .toInt(),
+ horizontalPaddingBetweenTasks =
+ (res.getDimensionPixelSize(
+ R.dimen.desktop_horizontal_padding_between_tasks
+ ) / widthScale)
+ .toInt(),
+ verticalPaddingBetweenTasks =
+ (res.getDimensionPixelSize(R.dimen.desktop_vertical_padding_between_tasks) /
+ heightScale)
+ .toInt(),
+ )
+
+ viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions, layoutConfig)
}
positionTaskWindows(updateLayout = true)
}
+ /**
+ * Calculates the scale factors for the desktop task view's width and height. This is determined
+ * by comparing the available task view dimensions (after accounting for margins like
+ * [thumbnailTopMarginPx]) against the total screen dimensions.
+ *
+ * @return A [Pair] where the first value is the scale factor for width and the second is for
+ * height.
+ */
+ private fun getScreenScaleFactors(): Pair {
+ val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
+ val taskViewWidth = layoutParams.width
+ val taskViewHeight = layoutParams.height - thumbnailTopMarginPx
+
+ val screenRect = getScreenRect()
+ val widthScale = taskViewWidth / screenRect.width().toFloat()
+ val heightScale = taskViewHeight / screenRect.height().toFloat()
+
+ return Pair(widthScale, heightScale)
+ }
+
+ /** Returns the dimensions of the screen. */
+ private fun getScreenRect(): Rect {
+ BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+ return Rect(0, 0, tempPointF.x.toInt(), tempPointF.y.toInt())
+ }
+
companion object {
private const val TAG = "DesktopTaskView"
private const val DEBUG = false
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCaseTest.kt
new file mode 100644
index 0000000000..f2038b28d9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCaseTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2025 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.recents.domain.usecase
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.domain.model.DesktopLayoutConfig
+import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Test for [OrganizeDesktopTasksUseCase] */
+@RunWith(AndroidJUnit4::class)
+class OrganizeDesktopTasksUseCaseTest {
+
+ private val useCase: OrganizeDesktopTasksUseCase = OrganizeDesktopTasksUseCase()
+ private val testLayoutConfig: DesktopLayoutConfig =
+ DesktopLayoutConfig(
+ topBottomMarginOneRow = 20,
+ topMarginMultiRows = 20,
+ bottomMarginMultiRows = 20,
+ leftRightMarginOneRow = 20,
+ leftRightMarginMultiRows = 20,
+ horizontalPaddingBetweenTasks = 10,
+ verticalPaddingBetweenTasks = 10,
+ )
+
+ @Test
+ fun test_emptyTaskBounds_returnsEmptyList() {
+ val desktopBounds = Rect(0, 0, 1000, 2000)
+ val taskBounds = emptyList()
+
+ val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig)
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun test_emptyDesktopBounds_returnsEmptyList() {
+ val desktopBounds = Rect(0, 0, 0, 0)
+ val taskBounds = listOf(DesktopTaskBoundsData(1, Rect(0, 0, 100, 100)))
+
+ val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig)
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun test_filtersOutTasksWithEmptyBounds() {
+ val desktopBounds = Rect(0, 0, 1000, 2000)
+ val taskBounds =
+ listOf(
+ DesktopTaskBoundsData(1, Rect(0, 0, 100, 100)),
+ DesktopTaskBoundsData(2, Rect()), // Empty bounds
+ DesktopTaskBoundsData(3, Rect(0, 0, 50, 50)),
+ )
+
+ val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig)
+ assertThat(result)
+ .isEqualTo(
+ listOf(
+ DesktopTaskBoundsData(1, Rect(20, 34, 980, 995)),
+ DesktopTaskBoundsData(3, Rect(20, 1005, 980, 1966)),
+ )
+ )
+ }
+
+ @Test
+ fun test_singleTask_isCenteredAndScaled() {
+ val desktopBounds = Rect(0, 0, 1000, 2000)
+ val originalAppRect = Rect(0, 0, 800, 1200)
+ val taskBounds = listOf(DesktopTaskBoundsData(1, originalAppRect))
+
+ val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig)
+
+ assertThat(result).hasSize(1)
+ val resultBounds = result[0].bounds
+ assertThat(resultBounds.width()).isGreaterThan(0)
+ assertThat(resultBounds.height()).isGreaterThan(0)
+
+ // Check aspect ratio is roughly preserved
+ val originalAspectRatio = originalAppRect.width().toFloat() / originalAppRect.height()
+ val resultAspectRatio = resultBounds.width().toFloat() / resultBounds.height()
+ assertThat(resultAspectRatio).isWithin(0.1f).of(originalAspectRatio)
+
+ // availableLayoutBounds will be Rect(20, 20, 980, 1980) after subtracting the margins.
+ // Check if the task is centered within effective layout bounds
+ val expectedTaskRect = Rect(25, 287, 975, 1713)
+ assertThat(result)
+ .isEqualTo(listOf(DesktopTaskBoundsData(taskId = 1, bounds = expectedTaskRect)))
+ }
+
+ @Test
+ fun test_multiTasks_formRows() {
+ val desktopBounds = Rect(0, 0, 1000, 2000)
+ // Make tasks wide enough so they likely won't all fit in one row
+ val taskRect = Rect(0, 0, 600, 400)
+ val taskBounds =
+ listOf(
+ DesktopTaskBoundsData(1, taskRect),
+ DesktopTaskBoundsData(2, taskRect),
+ DesktopTaskBoundsData(3, taskRect),
+ )
+
+ val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig)
+ assertThat(result).hasSize(3)
+ val bounds1 = result[0].bounds
+
+ // Basic checks: positive dimensions, aspect ratio
+ result.forEachIndexed { index, data ->
+ assertThat(data.bounds.width()).isGreaterThan(0)
+ assertThat(data.bounds.height()).isGreaterThan(0)
+ val originalAspectRatio = taskRect.width().toFloat() / taskRect.height()
+ val resultAspectRatio = data.bounds.width().toFloat() / data.bounds.height()
+ assertThat(resultAspectRatio).isWithin(0.1f).of(originalAspectRatio)
+ }
+
+ // Expected bounds, based on the current implementation.
+ // The tasks are expected to be arranged in 3 rows.
+ val expectedTask1Bounds = Rect(20, 30, 980, 670)
+ val expectedTask2Bounds = Rect(20, 680, 980, 1320)
+ val expectedTask3Bounds = Rect(20, 1330, 980, 1970)
+ val expectedResult =
+ listOf(
+ DesktopTaskBoundsData(1, expectedTask1Bounds),
+ DesktopTaskBoundsData(2, expectedTask2Bounds),
+ DesktopTaskBoundsData(3, expectedTask3Bounds),
+ )
+ assertThat(result).isEqualTo(expectedResult)
+ }
+}