• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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