From a66268c9fcbb3e22052ceca6570a6fc679e594a7 Mon Sep 17 00:00:00 2001 From: "Xiaoqian (Daisy) Dai" Date: Fri, 9 May 2025 15:38:08 -0700 Subject: [PATCH] desktop-exploded-view: Fix various values for Task windows in desktop tile. To match with the UX spec, this CL updates the insets based on the number of tasks within a tile. It also sets a maximum height to the task window when multiple apps are available. Flag: com.android.launcher3.enable_desktop_exploded_view Test: OrganizeDesktopTasksUseCaseTest Bug: 414614454 Change-Id: Icfecc1e41dfd0b95bae10b495adae95fe10dfef2 --- quickstep/res/values/dimens.xml | 8 + .../domain/model/DesktopLayoutConfig.kt | 40 +++++ .../usecase/OrganizeDesktopTasksUseCase.kt | 128 ++++++++++++--- .../ui/viewmodel/DesktopTaskViewModel.kt | 11 +- .../quickstep/views/DesktopTaskView.kt | 113 ++++++++++---- .../OrganizeDesktopTasksUseCaseTest.kt | 146 ++++++++++++++++++ 6 files changed, 393 insertions(+), 53 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/recents/domain/model/DesktopLayoutConfig.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCaseTest.kt 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) + } +}