1 /* <lambda>null2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.recents.domain.usecase 18 19 import android.graphics.Rect 20 import android.graphics.RectF 21 import androidx.core.graphics.toRect 22 import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData 23 24 /** This usecase is responsible for organizing desktop windows in a non-overlapping way. */ 25 class OrganizeDesktopTasksUseCase { 26 /** 27 * Run to layout [taskBounds] within the screen [desktopBounds]. Layout is done in 2 stages: 28 * 1. Optimal height is determined. In this stage height is bisected to find maximum height 29 * which still allows all the windows to fit. 30 * 2. Row widths are balanced. In this stage the available width is reduced until some windows 31 * are no longer fitting or until the difference between the narrowest and the widest rows 32 * starts growing. Overall this achieves the goals of maximum size for previews (or maximum 33 * row height which is equivalent assuming fixed height), balanced rows and minimal wasted 34 * space. 35 */ 36 fun run( 37 desktopBounds: Rect, 38 taskBounds: List<DesktopTaskBoundsData>, 39 ): List<DesktopTaskBoundsData> { 40 if (desktopBounds.isEmpty || taskBounds.isEmpty()) { 41 return emptyList() 42 } 43 44 // Filter out [taskBounds] with empty rects before calculating layout. 45 val validTaskBounds = taskBounds.filterNot { it.bounds.isEmpty } 46 47 if (validTaskBounds.isEmpty()) { 48 return emptyList() 49 } 50 51 val availableLayoutBounds = desktopBounds.getLayoutEffectiveBounds() 52 val resultRects = findOptimalHeightAndBalancedWidth(availableLayoutBounds, validTaskBounds) 53 54 centerTaskWindows( 55 availableLayoutBounds, 56 resultRects.maxOf { it.bottom }.toInt(), 57 resultRects, 58 ) 59 60 val result = mutableListOf<DesktopTaskBoundsData>() 61 for (i in validTaskBounds.indices) { 62 result.add(DesktopTaskBoundsData(validTaskBounds[i].taskId, resultRects[i].toRect())) 63 } 64 return result 65 } 66 67 /** Calculates the effective bounds for layout by applying insets to the raw desktop bounds. */ 68 private fun Rect.getLayoutEffectiveBounds() = 69 Rect(this).apply { inset(OVERVIEW_INSET_TOP_BOTTOM, OVERVIEW_INSET_LEFT_RIGHT) } 70 71 /** 72 * Determines the optimal height for task windows and balances the row widths to minimize wasted 73 * space. Returns the bounds for each task window after layout. 74 */ 75 private fun findOptimalHeightAndBalancedWidth( 76 availableLayoutBounds: Rect, 77 validTaskBounds: List<DesktopTaskBoundsData>, 78 ): List<RectF> { 79 // Right bound of the narrowest row. 80 var minRight: Int 81 // Right bound of the widest row. 82 var maxRight: Int 83 84 // Keep track of the difference between the narrowest and the widest row. 85 // Initially this is set to the worst it can ever be assuming the windows fit. 86 var widthDiff = availableLayoutBounds.width() 87 88 // Initially allow the windows to occupy all available width. Shrink this available space 89 // horizontally to find the breakdown into rows that achieves the minimal [widthDiff]. 90 var rightBound = availableLayoutBounds.right 91 92 // Determine the optimal height bisecting between [lowHeight] and [highHeight]. Once this 93 // optimal height is known, [heightFixed] is set to `true` and the rows are balanced by 94 // repeatedly squeezing the widest row to cause windows to overflow to the subsequent rows. 95 var lowHeight = VERTICAL_SPACE_BETWEEN_TASKS 96 var highHeight = maxOf(lowHeight, availableLayoutBounds.height() + 1) 97 var optimalHeight = 0.5f * (lowHeight + highHeight) 98 var heightFixed = false 99 100 // Repeatedly try to fit the windows [resultRects] within [rightBound]. If a maximum 101 // [optimalHeight] is found such that all window [resultRects] fit, this fitting continues 102 // while shrinking the [rightBound] in order to balance the rows. If the windows fit the 103 // [rightBound] would have been decremented at least once so it needs to be incremented once 104 // before getting out of this loop and one additional pass made to actually fit the 105 // [resultRects]. If the [resultRects] cannot fit (e.g. there are too many windows) the 106 // bisection will still finish and we might increment the [rightBound] one pixel extra 107 // which is acceptable since there is an unused margin on the right. 108 var makeLastAdjustment = false 109 var resultRects: List<RectF> 110 111 while (true) { 112 val fitWindowResult = 113 fitWindowRectsInBounds( 114 Rect(availableLayoutBounds).apply { right = rightBound }, 115 validTaskBounds, 116 minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()), 117 ) 118 val allWindowsFit = fitWindowResult.allWindowsFit 119 resultRects = fitWindowResult.calculatedBounds 120 minRight = fitWindowResult.minRight 121 maxRight = fitWindowResult.maxRight 122 123 if (heightFixed) { 124 if (!allWindowsFit) { 125 // Revert the previous change to [rightBound] and do one last pass. 126 rightBound++ 127 makeLastAdjustment = true 128 break 129 } 130 // Break if all the windows are zero-width at the current scale. 131 if (maxRight <= availableLayoutBounds.left) { 132 break 133 } 134 } else { 135 // Find the optimal row height bisecting between [lowHeight] and [highHeight]. 136 if (allWindowsFit) { 137 lowHeight = optimalHeight.toInt() 138 } else { 139 highHeight = optimalHeight.toInt() 140 } 141 optimalHeight = 0.5f * (lowHeight + highHeight) 142 // When height can no longer be improved, start balancing the rows. 143 if (optimalHeight.toInt() == lowHeight) { 144 heightFixed = true 145 } 146 } 147 148 if (allWindowsFit && heightFixed) { 149 if (maxRight - minRight <= widthDiff) { 150 // Row alignment is getting better. Try to shrink the [rightBound] in order to 151 // squeeze the widest row. 152 rightBound = maxRight - 1 153 widthDiff = maxRight - minRight 154 } else { 155 // Row alignment is getting worse. 156 // Revert the previous change to [rightBound] and do one last pass. 157 rightBound++ 158 makeLastAdjustment = true 159 break 160 } 161 } 162 } 163 164 // Once the windows no longer fit, the change to [rightBound] was reverted. Perform one last 165 // pass to position the [resultRects]. 166 if (makeLastAdjustment) { 167 val fitWindowResult = 168 fitWindowRectsInBounds( 169 Rect(availableLayoutBounds).apply { right = rightBound }, 170 validTaskBounds, 171 minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()), 172 ) 173 resultRects = fitWindowResult.calculatedBounds 174 } 175 176 return resultRects 177 } 178 179 /** 180 * Data structure to hold the returned result of [fitWindowRectsInBounds] function. 181 * [allWindowsFit] specifies whether all windows can be fit into the provided layout bounds. 182 * [calculatedBounds] specifies the output bounds for all provided task windows. [minRight] 183 * specifies the right bound of the narrowest row. [maxRight] specifies the right bound of the 184 * widest rows. 185 */ 186 data class FitWindowResult( 187 val allWindowsFit: Boolean, 188 val calculatedBounds: List<RectF>, 189 val minRight: Int, 190 val maxRight: Int, 191 ) 192 193 /** 194 * Attempts to fit all [taskBounds] inside [layoutBounds]. The method ensures that the returned 195 * output bounds list has appropriate size and populates it with the values placing task windows 196 * next to each other left-to-right in rows of equal [optimalWindowHeight]. 197 */ 198 private fun fitWindowRectsInBounds( 199 layoutBounds: Rect, 200 taskBounds: List<DesktopTaskBoundsData>, 201 optimalWindowHeight: Int, 202 ): FitWindowResult { 203 val numTasks = taskBounds.size 204 val outRects = mutableListOf<RectF>() 205 206 // Start in the top-left corner of [layoutBounds]. 207 var left = layoutBounds.left 208 var top = layoutBounds.top 209 210 // Right bound of the narrowest row. 211 var minRight = layoutBounds.right 212 // Right bound of the widest row. 213 var maxRight = layoutBounds.left 214 215 var allWindowsFit = true 216 for (i in 0 until numTasks) { 217 val taskBounds = taskBounds[i].bounds 218 219 // Use the height to calculate the width 220 val scale = optimalWindowHeight / taskBounds.height().toFloat() 221 val width = (taskBounds.width() * scale).toInt() 222 val optimalRowHeight = optimalWindowHeight + VERTICAL_SPACE_BETWEEN_TASKS 223 224 if ((left + width + HORIZONTAL_SPACE_BETWEEN_TASKS) > layoutBounds.right) { 225 // Move to the next row if possible. 226 minRight = minOf(minRight, left) 227 maxRight = maxOf(maxRight, left) 228 top += optimalRowHeight 229 230 // Check if the new row reaches the bottom or if the first item in the new 231 // row does not fit within the available width. 232 if ( 233 (top + optimalRowHeight) > layoutBounds.bottom || 234 layoutBounds.left + width + HORIZONTAL_SPACE_BETWEEN_TASKS > 235 layoutBounds.right 236 ) { 237 allWindowsFit = false 238 break 239 } 240 left = layoutBounds.left 241 } 242 243 // Position the current rect. 244 outRects.add( 245 RectF( 246 left.toFloat(), 247 top.toFloat(), 248 (left + width).toFloat(), 249 (top + optimalWindowHeight).toFloat(), 250 ) 251 ) 252 253 // Increment horizontal position. 254 left += (width + HORIZONTAL_SPACE_BETWEEN_TASKS) 255 } 256 257 // Update the narrowest and widest row width for the last row. 258 minRight = minOf(minRight, left) 259 maxRight = maxOf(maxRight, left) 260 261 return FitWindowResult(allWindowsFit, outRects, minRight, maxRight) 262 } 263 264 /** Centers task windows in the center of Overview. */ 265 private fun centerTaskWindows(layoutBounds: Rect, maxBottom: Int, outWindowRects: List<RectF>) { 266 if (outWindowRects.isEmpty()) { 267 return 268 } 269 270 val currentRowUnionRange = RectF(outWindowRects[0]) 271 var currentRowY = outWindowRects[0].top 272 var currentRowFirstItemIndex = 0 273 val offsetY = (layoutBounds.bottom - maxBottom) / 2f 274 275 // Batch process to center overview desktop task windows within the same row. 276 fun batchCenterDesktopTaskWindows(endIndex: Int) { 277 // Calculate the shift amount required to center the desktop task items. 278 val rangeCenterX = (currentRowUnionRange.left + currentRowUnionRange.right) / 2f 279 val currentDiffX = (layoutBounds.centerX() - rangeCenterX).coerceAtLeast(0f) 280 for (j in currentRowFirstItemIndex until endIndex) { 281 outWindowRects[j].offset(currentDiffX, offsetY) 282 } 283 } 284 285 outWindowRects.forEachIndexed { index, rect -> 286 if (rect.top != currentRowY) { 287 // As a new row begins processing, batch-shift the previous row's rects 288 // and reset its parameters. 289 batchCenterDesktopTaskWindows(index) 290 currentRowUnionRange.set(rect) 291 currentRowY = rect.top 292 currentRowFirstItemIndex = index 293 } 294 295 // Extend the range by adding the [rect]'s width and extra in-between items 296 // spacing. 297 currentRowUnionRange.right = rect.right 298 } 299 300 // Post-processing rects in the last row. 301 batchCenterDesktopTaskWindows(outWindowRects.size) 302 } 303 304 private companion object { 305 const val VERTICAL_SPACE_BETWEEN_TASKS = 24 306 const val HORIZONTAL_SPACE_BETWEEN_TASKS = 24 307 const val OVERVIEW_INSET_TOP_BOTTOM = 16 308 const val OVERVIEW_INSET_LEFT_RIGHT = 16 309 const val MAXIMUM_TASK_HEIGHT = 800 310 } 311 } 312