1 /*
<lambda>null2 * Copyright (C) 2022 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.wm.shell.desktopmode
18
19 import android.graphics.Rect
20 import android.graphics.Region
21 import android.util.ArrayMap
22 import android.util.ArraySet
23 import android.util.SparseArray
24 import android.view.Display.INVALID_DISPLAY
25 import android.window.DesktopExperienceFlags
26 import android.window.DesktopModeFlags
27 import androidx.core.util.forEach
28 import androidx.core.util.valueIterator
29 import com.android.internal.annotations.VisibleForTesting
30 import com.android.internal.protolog.ProtoLog
31 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
32 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
33 import com.android.wm.shell.shared.annotations.ShellMainThread
34 import java.io.PrintWriter
35 import java.util.concurrent.Executor
36 import java.util.function.Consumer
37 import kotlin.collections.component1
38 import kotlin.collections.component2
39 import kotlin.collections.forEach
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.launch
42
43 /** Tracks desktop data for Android Desktop Windowing. */
44 class DesktopRepository(
45 private val persistentRepository: DesktopPersistentRepository,
46 @ShellMainThread private val mainCoroutineScope: CoroutineScope,
47 val userId: Int,
48 ) {
49 /** A display that supports desktops. */
50 private data class DesktopDisplay(
51 val displayId: Int,
52 val orderedDesks: MutableSet<Desk> = mutableSetOf(),
53 // TODO: b/389960283 - update on desk activation / deactivation.
54 var activeDeskId: Int? = null,
55 )
56
57 /**
58 * Task data tracked per desk.
59 *
60 * @property activeTasks task ids of active tasks currently or previously visible in the desk.
61 * Tasks become inactive when task closes or when the desk becomes inactive.
62 * @property visibleTasks task ids for active freeform tasks that are currently visible. There
63 * might be other active tasks in a desk that are not visible.
64 * @property minimizedTasks task ids for active freeform tasks that are currently minimized.
65 * @property closingTasks task ids for tasks that are going to close, but are currently visible.
66 * @property freeformTasksInZOrder list of current freeform task ids ordered from top to bottom
67 * @property fullImmersiveTaskId the task id of the desk's task that is in full-immersive mode.
68 * @property topTransparentFullscreenTaskId the task id of any current top transparent
69 * fullscreen task launched on top of the desk. Cleared when the transparent task is closed or
70 * sent to back. (top is at index 0).
71 * @property leftTiledTaskId task id of the task tiled on the left.
72 * @property rightTiledTaskId task id of the task tiled on the right.
73 */
74 private data class Desk(
75 val deskId: Int,
76 val displayId: Int,
77 val activeTasks: ArraySet<Int> = ArraySet(),
78 val visibleTasks: ArraySet<Int> = ArraySet(),
79 val minimizedTasks: ArraySet<Int> = ArraySet(),
80 // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
81 val closingTasks: ArraySet<Int> = ArraySet(),
82 val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
83 var fullImmersiveTaskId: Int? = null,
84 var topTransparentFullscreenTaskId: Int? = null,
85 var leftTiledTaskId: Int? = null,
86 var rightTiledTaskId: Int? = null,
87 ) {
88 fun deepCopy(): Desk =
89 Desk(
90 deskId = deskId,
91 displayId = displayId,
92 activeTasks = ArraySet(activeTasks),
93 visibleTasks = ArraySet(visibleTasks),
94 minimizedTasks = ArraySet(minimizedTasks),
95 closingTasks = ArraySet(closingTasks),
96 freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
97 fullImmersiveTaskId = fullImmersiveTaskId,
98 topTransparentFullscreenTaskId = topTransparentFullscreenTaskId,
99 leftTiledTaskId = leftTiledTaskId,
100 rightTiledTaskId = rightTiledTaskId,
101 )
102
103 // TODO: b/362720497 - remove when multi-desktops is enabled where instances aren't
104 // reusable.
105 fun clear() {
106 activeTasks.clear()
107 visibleTasks.clear()
108 minimizedTasks.clear()
109 closingTasks.clear()
110 freeformTasksInZOrder.clear()
111 fullImmersiveTaskId = null
112 topTransparentFullscreenTaskId = null
113 leftTiledTaskId = null
114 rightTiledTaskId = null
115 }
116 }
117
118 private val deskChangeListeners = ArrayMap<DeskChangeListener, Executor>()
119 private val activeTasksListeners = ArraySet<ActiveTasksListener>()
120 private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
121
122 /* Tracks corner/caption regions of desktop tasks, used to determine gesture exclusion. */
123 private val desktopExclusionRegions = SparseArray<Region>()
124
125 /* Tracks last bounds of task before toggled to stable bounds. */
126 private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>()
127
128 /* Tracks last bounds of task before it is minimized. */
129 private val boundsBeforeMinimizeByTaskId = SparseArray<Rect>()
130
131 /* Tracks last bounds of task before toggled to immersive state. */
132 private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>()
133
134 private var desktopGestureExclusionListener: Consumer<Region>? = null
135 private var desktopGestureExclusionExecutor: Executor? = null
136
137 private val desktopData: DesktopData =
138 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
139 MultiDesktopData()
140 } else {
141 SingleDesktopData()
142 }
143
144 /** Adds a listener to be notified of updates about desk changes. */
145 fun addDeskChangeListener(listener: DeskChangeListener, executor: Executor) {
146 deskChangeListeners[listener] = executor
147 }
148
149 /** Adds [activeTasksListener] to be notified of updates to active tasks. */
150 fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) {
151 activeTasksListeners.add(activeTasksListener)
152 }
153
154 /** Adds [visibleTasksListener] to be notified of updates to visible tasks. */
155 fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
156 visibleTasksListeners[visibleTasksListener] = executor
157 desktopData
158 .desksSequence()
159 .groupBy { it.displayId }
160 .keys
161 .forEach { displayId ->
162 val visibleTaskCount = getVisibleTaskCount(displayId)
163 executor.execute {
164 visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTaskCount)
165 }
166 }
167 }
168
169 /** Updates tasks changes on all the active task listeners for given display id. */
170 private fun updateActiveTasksListeners(displayId: Int) {
171 activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) }
172 }
173
174 /** Returns a list of all [Desk]s in the repository. */
175 private fun desksSequence(): Sequence<Desk> = desktopData.desksSequence()
176
177 /** Returns the number of desks in the given display. */
178 fun getNumberOfDesks(displayId: Int) = desktopData.getNumberOfDesks(displayId)
179
180 /** Returns the display the given desk is in. */
181 fun getDisplayForDesk(deskId: Int) = desktopData.getDisplayForDesk(deskId)
182
183 /** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */
184 fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) {
185 desktopGestureExclusionListener = regionListener
186 desktopGestureExclusionExecutor = executor
187 executor.execute {
188 desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
189 }
190 }
191
192 /** Creates a new merged region representative of all exclusion regions in all desktop tasks. */
193 private fun calculateDesktopExclusionRegion(): Region {
194 val desktopExclusionRegion = Region()
195 desktopExclusionRegions.valueIterator().forEach { taskExclusionRegion ->
196 desktopExclusionRegion.op(taskExclusionRegion, Region.Op.UNION)
197 }
198 return desktopExclusionRegion
199 }
200
201 /** Removes the previously registered listener. */
202 fun removeDeskChangeListener(listener: DeskChangeListener) {
203 deskChangeListeners.remove(listener)
204 }
205
206 /** Remove the previously registered [activeTasksListener] */
207 fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
208 activeTasksListeners.remove(activeTasksListener)
209 }
210
211 /** Removes the previously registered [visibleTasksListener]. */
212 fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
213 visibleTasksListeners.remove(visibleTasksListener)
214 }
215
216 /** Adds the given desk under the given display. */
217 fun addDesk(displayId: Int, deskId: Int) {
218 logD("addDesk for displayId=%d and deskId=%d", displayId, deskId)
219 desktopData.createDesk(displayId, deskId)
220 deskChangeListeners.forEach { (listener, executor) ->
221 executor.execute { listener.onDeskAdded(displayId = displayId, deskId = deskId) }
222 }
223 }
224
225 /** Returns the ids of the existing desks in the given display. */
226 @VisibleForTesting
227 fun getDeskIds(displayId: Int): Set<Int> =
228 desktopData.desksSequence(displayId).map { desk -> desk.deskId }.toSet()
229
230 /** Returns all the ids of all desks in all displays. */
231 fun getAllDeskIds(): Set<Int> = desktopData.desksSequence().map { desk -> desk.deskId }.toSet()
232
233 /** Returns the id of the default desk in the given display. */
234 fun getDefaultDeskId(displayId: Int): Int? = getDefaultDesk(displayId)?.deskId
235
236 /** Returns the default desk in the given display. */
237 private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId)
238
239 /** Returns whether the given desk is active in its display. */
240 fun isDeskActive(deskId: Int): Boolean =
241 desktopData.getAllActiveDesks().any { desk -> desk.deskId == deskId }
242
243 /** Sets the given desk as the active one in the given display. */
244 fun setActiveDesk(displayId: Int, deskId: Int) {
245 logD("setActiveDesk for displayId=%d and deskId=%d", displayId, deskId)
246 val oldActiveDeskId = desktopData.getActiveDesk(displayId)?.deskId ?: INVALID_DESK_ID
247 desktopData.setActiveDesk(displayId = displayId, deskId = deskId)
248 deskChangeListeners.forEach { (listener, executor) ->
249 executor.execute {
250 listener.onActiveDeskChanged(
251 displayId = displayId,
252 newActiveDeskId = deskId,
253 oldActiveDeskId = oldActiveDeskId,
254 )
255 }
256 }
257 }
258
259 /** Sets the given desk as inactive if it was active. */
260 fun setDeskInactive(deskId: Int) {
261 val displayId = desktopData.getDisplayForDesk(deskId)
262 val activeDeskId = desktopData.getActiveDesk(displayId)?.deskId ?: INVALID_DESK_ID
263 if (activeDeskId == INVALID_DESK_ID || activeDeskId != deskId) {
264 // Desk wasn't active.
265 return
266 }
267 desktopData.setDeskInactive(deskId)
268 deskChangeListeners.forEach { (listener, executor) ->
269 executor.execute {
270 listener.onActiveDeskChanged(
271 displayId = displayId,
272 newActiveDeskId = INVALID_DESK_ID,
273 oldActiveDeskId = deskId,
274 )
275 }
276 }
277 }
278
279 /** Register a left tiled task to desktop state. */
280 fun addLeftTiledTask(displayId: Int, taskId: Int) {
281 logD("addLeftTiledTask for displayId=%d, taskId=%d", displayId, taskId)
282 val activeDesk =
283 checkNotNull(desktopData.getDefaultDesk(displayId)) {
284 "Expected desk in display: $displayId"
285 }
286 addLeftTiledTaskToDesk(displayId, taskId, activeDesk.deskId)
287 }
288
289 private fun addLeftTiledTaskToDesk(displayId: Int, taskId: Int, deskId: Int) {
290 logD("addLeftTiledTaskToDesk for displayId=%d, taskId=%d", displayId, taskId)
291 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
292 desk.leftTiledTaskId = taskId
293 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
294 updatePersistentRepository(displayId)
295 }
296 }
297
298 /** Register a right tiled task to desktop state. */
299 fun addRightTiledTask(displayId: Int, taskId: Int) {
300 logD("addRightTiledTask for displayId=%d, taskId=%d", displayId, taskId)
301 val activeDesk =
302 checkNotNull(desktopData.getDefaultDesk(displayId)) {
303 "Expected desk in display: $displayId"
304 }
305 addRightTiledTaskToDesk(displayId, taskId, activeDesk.deskId)
306 }
307
308 private fun addRightTiledTaskToDesk(displayId: Int, taskId: Int, deskId: Int) {
309 logD("addRightTiledTaskToDesk for displayId=%d, taskId=%d", displayId, taskId)
310 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
311 desk.rightTiledTaskId = taskId
312 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
313 updatePersistentRepository(displayId)
314 }
315 }
316
317 /** Gets a registered left tiled task to desktop state or returns null. */
318 fun getLeftTiledTask(displayId: Int): Int? {
319 logD("getLeftTiledTask for displayId=%d", displayId)
320 val activeDesk =
321 checkNotNull(desktopData.getDefaultDesk(displayId)) {
322 "Expected desk in display: $displayId"
323 }
324 val deskId = activeDesk.deskId
325 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
326 return desk.leftTiledTaskId
327 }
328
329 /** gets a registered right tiled task to desktop state or returns null. */
330 fun getRightTiledTask(displayId: Int): Int? {
331 logD("getRightTiledTask for displayId=%d", displayId)
332 val activeDesk =
333 checkNotNull(desktopData.getDefaultDesk(displayId)) {
334 "Expected desk in display: $displayId"
335 }
336 val deskId = activeDesk.deskId
337 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
338 return desk.rightTiledTaskId
339 }
340
341 /* Unregisters a left tiled task from desktop state. */
342 fun removeLeftTiledTask(displayId: Int) {
343 logD("removeLeftTiledTask for displayId=%d", displayId)
344 val activeDesk =
345 checkNotNull(desktopData.getDefaultDesk(displayId)) {
346 "Expected desk in display: $displayId"
347 }
348 removeLeftTiledTaskFromDesk(displayId, activeDesk.deskId)
349 }
350
351 private fun removeLeftTiledTaskFromDesk(displayId: Int, deskId: Int) {
352 logD("removeLeftTiledTaskToDesk for displayId=%d", displayId)
353 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
354 desk.leftTiledTaskId = null
355 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
356 updatePersistentRepository(displayId)
357 }
358 }
359
360 /* Unregisters a right tiled task from desktop state. */
361 fun removeRightTiledTask(displayId: Int) {
362 logD("removeRightTiledTask for displayId=%d", displayId)
363 val activeDesk =
364 checkNotNull(desktopData.getDefaultDesk(displayId)) {
365 "Expected desk in display: $displayId"
366 }
367 removeRightTiledTaskFromDesk(displayId, activeDesk.deskId)
368 }
369
370 private fun removeRightTiledTaskFromDesk(displayId: Int, deskId: Int) {
371 logD("removeRightTiledTaskFromDesk for displayId=%d", displayId)
372 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
373 desk.rightTiledTaskId = null
374 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
375 updatePersistentRepository(displayId)
376 }
377 }
378
379 /** Returns the id of the active desk in the given display, if any. */
380 fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId
381
382 /** Returns the id of the desk to which this task belongs. */
383 fun getDeskIdForTask(taskId: Int): Int? =
384 desktopData.desksSequence().find { desk -> desk.activeTasks.contains(taskId) }?.deskId
385
386 /**
387 * Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk.
388 *
389 * TODO: b/389960283 - add explicit [deskId] argument.
390 */
391 fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) {
392 logD("addTask for displayId=%d, taskId=%d, isVisible=%b", displayId, taskId, isVisible)
393 val activeDesk =
394 checkNotNull(desktopData.getDefaultDesk(displayId)) {
395 "Expected desk in display: $displayId"
396 }
397 addTaskToDesk(displayId = displayId, deskId = activeDesk.deskId, taskId = taskId, isVisible)
398 }
399
400 fun addTaskToDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) {
401 logD(
402 "addTaskToDesk for displayId=%d, deskId=%d, taskId=%d, isVisible=%b",
403 displayId,
404 deskId,
405 taskId,
406 isVisible,
407 )
408 addOrMoveTaskToTopOfDesk(displayId = displayId, deskId = deskId, taskId = taskId)
409 addActiveTaskToDesk(displayId = displayId, deskId = deskId, taskId = taskId)
410 updateTaskInDesk(
411 displayId = displayId,
412 deskId = deskId,
413 taskId = taskId,
414 isVisible = isVisible,
415 )
416 }
417
418 private fun addActiveTaskToDesk(displayId: Int, deskId: Int, taskId: Int) {
419 logD(
420 "addActiveTaskToDesk for displayId=%d, deskId=%d, taskId=%d",
421 displayId,
422 deskId,
423 taskId,
424 )
425 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
426
427 // Removes task if it is active on another desk excluding this desk.
428 removeActiveTask(taskId, excludedDeskId = deskId)
429
430 if (desk.activeTasks.add(taskId)) {
431 logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, deskId)
432 updateActiveTasksListeners(displayId)
433 }
434 }
435
436 /** Removes task from active task list of desks excluding the [excludedDeskId]. */
437 @VisibleForTesting
438 fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) {
439 logD("removeActiveTask for taskId=%d, excludedDeskId=%d", taskId, excludedDeskId)
440 val affectedDisplays = mutableSetOf<Int>()
441 desktopData
442 .desksSequence()
443 .filter { desk -> desk.deskId != excludedDeskId }
444 .forEach { desk ->
445 val removed = removeActiveTaskFromDesk(desk.deskId, taskId, notifyListeners = false)
446 if (removed) {
447 logD(
448 "Removed active task=%d displayId=%d deskId=%d",
449 taskId,
450 desk.displayId,
451 desk.deskId,
452 )
453 affectedDisplays.add(desk.displayId)
454 }
455 }
456 affectedDisplays.forEach { displayId -> updateActiveTasksListeners(displayId) }
457 }
458
459 private fun removeActiveTaskFromDesk(
460 deskId: Int,
461 taskId: Int,
462 notifyListeners: Boolean = true,
463 ): Boolean {
464 logD("removeActiveTaskFromDesk for deskId=%d, taskId=%d", deskId, taskId)
465 val desk = desktopData.getDesk(deskId) ?: return false
466 if (desk.activeTasks.remove(taskId)) {
467 logD("Removed active task=%d from deskId=%d", taskId, desk.deskId)
468 if (notifyListeners) {
469 updateActiveTasksListeners(desk.displayId)
470 }
471 return true
472 }
473 return false
474 }
475
476 /** Adds given task to the closing task list of its desk. */
477 fun addClosingTask(displayId: Int, deskId: Int?, taskId: Int) {
478 val desk =
479 deskId?.let { desktopData.getDesk(it) }
480 ?: checkNotNull(desktopData.getActiveDesk(displayId)) {
481 "Expected active desk in display: $displayId"
482 }
483 if (desk.closingTasks.add(taskId)) {
484 logD("Added closing task=%d displayId=%d deskId=%d", taskId, displayId, desk.deskId)
485 } else {
486 // If the task hasn't been removed from closing list after it disappeared.
487 logW(
488 "Task with taskId=%d displayId=%d deskId=%d is already closing",
489 taskId,
490 displayId,
491 desk.deskId,
492 )
493 }
494 }
495
496 /** Removes task from the list of closing tasks for all desks. */
497 fun removeClosingTask(taskId: Int) {
498 desktopData.forAllDesks { desk ->
499 if (desk.closingTasks.remove(taskId)) {
500 logD("Removed closing task=%d deskId=%d", taskId, desk.deskId)
501 }
502 }
503 }
504
505 fun isActiveTask(taskId: Int) = desksSequence().any { taskId in it.activeTasks }
506
507 @VisibleForTesting
508 fun isActiveTaskInDesk(taskId: Int, deskId: Int): Boolean {
509 val desk = desktopData.getDesk(deskId) ?: return false
510 return taskId in desk.activeTasks
511 }
512
513 fun isClosingTask(taskId: Int) = desksSequence().any { taskId in it.closingTasks }
514
515 fun isVisibleTask(taskId: Int) = desksSequence().any { taskId in it.visibleTasks }
516
517 @VisibleForTesting
518 fun isVisibleTaskInDesk(taskId: Int, deskId: Int): Boolean {
519 val desk = desktopData.getDesk(deskId) ?: return false
520 return taskId in desk.visibleTasks
521 }
522
523 fun isMinimizedTask(taskId: Int) = desksSequence().any { taskId in it.minimizedTasks }
524
525 /**
526 * Checks if a task is the only visible, non-closing, non-minimized task on the active desk of
527 * the given display, or any display's active desk if [displayId] is [INVALID_DISPLAY].
528 *
529 * TODO: b/389960283 - consider forcing callers to use [isOnlyVisibleNonClosingTaskInDesk] with
530 * an explicit desk id instead of using this function and defaulting to the active one.
531 */
532 fun isOnlyVisibleNonClosingTask(taskId: Int, displayId: Int = INVALID_DISPLAY): Boolean {
533 val activeDesks =
534 if (displayId != INVALID_DISPLAY) {
535 setOfNotNull(desktopData.getActiveDesk(displayId))
536 } else {
537 desktopData.getAllActiveDesks()
538 }
539 return activeDesks.any { desk ->
540 isOnlyVisibleNonClosingTaskInDesk(
541 taskId = taskId,
542 deskId = desk.deskId,
543 displayId = desk.displayId,
544 )
545 }
546 }
547
548 /**
549 * Checks if a task is the only visible, non-closing, non-minimized task on the given desk of
550 * the given display.
551 */
552 fun isOnlyVisibleNonClosingTaskInDesk(taskId: Int, deskId: Int, displayId: Int): Boolean {
553 val desk = desktopData.getDesk(deskId) ?: return false
554 return desk.visibleTasks
555 .subtract(desk.closingTasks)
556 .subtract(desk.minimizedTasks)
557 .singleOrNull() == taskId
558 }
559
560 /** Whether the task is the only visible desktop task in the display. */
561 fun isOnlyVisibleTask(taskId: Int, displayId: Int): Boolean {
562 val desk = desktopData.getActiveDesk(displayId) ?: return false
563 return desk.visibleTasks.size == 1 && desk.visibleTasks.single() == taskId
564 }
565
566 /** Whether the display has only one visible desktop task. */
567 fun hasOnlyOneVisibleTask(displayId: Int): Boolean = getVisibleTaskCount(displayId) == 1
568
569 @VisibleForTesting
570 fun getActiveTasks(displayId: Int): ArraySet<Int> =
571 ArraySet(desktopData.getActiveDesk(displayId)?.activeTasks)
572
573 /**
574 * Returns the minimized tasks in the given display's active desk.
575 *
576 * TODO: b/389960283 - migrate callers to [getMinimizedTaskIdsInDesk].
577 */
578 fun getMinimizedTasks(displayId: Int): ArraySet<Int> =
579 ArraySet(desktopData.getActiveDesk(displayId)?.minimizedTasks)
580
581 @VisibleForTesting
582 fun getMinimizedTaskIdsInDesk(deskId: Int): ArraySet<Int> =
583 ArraySet(desktopData.getDesk(deskId)?.minimizedTasks)
584
585 /**
586 * Returns all active non-minimized tasks for [displayId] ordered from top to bottom.
587 *
588 * TODO: b/389960283 - migrate callers to [getExpandedTasksIdsInDeskOrdered].
589 */
590 fun getExpandedTasksOrdered(displayId: Int): List<Int> =
591 getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) }
592
593 /** Returns all active non-minimized tasks for [deskId] ordered from top to bottom. */
594 fun getExpandedTasksIdsInDeskOrdered(deskId: Int): List<Int> =
595 getFreeformTasksIdsInDeskInZOrder(deskId).filter { !isMinimizedTask(it) }
596
597 /**
598 * Returns the count of active non-minimized tasks for [displayId].
599 *
600 * TODO: b/389960283 - add explicit [deskId] argument.
601 */
602 fun getExpandedTaskCount(displayId: Int): Int {
603 return getActiveTasks(displayId).count { !isMinimizedTask(it) }
604 }
605
606 /**
607 * Returns a list of freeform tasks, ordered from top-bottom (top at index 0).
608 *
609 * TODO: b/389960283 - migrate callers to [getFreeformTasksIdsInDeskInZOrder].
610 */
611 @VisibleForTesting
612 fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> =
613 ArrayList(desktopData.getDefaultDesk(displayId)?.freeformTasksInZOrder ?: emptyList())
614
615 @VisibleForTesting
616 fun getFreeformTasksIdsInDeskInZOrder(deskId: Int): ArrayList<Int> =
617 ArrayList(desktopData.getDesk(deskId)?.freeformTasksInZOrder ?: emptyList())
618
619 /** Returns the tasks inside the given desk. */
620 fun getActiveTaskIdsInDesk(deskId: Int): Set<Int> =
621 desktopData.getDesk(deskId)?.activeTasks?.toSet()
622 ?: run {
623 logW("getTasksInDesk: could not find desk: deskId=%d", deskId)
624 emptySet()
625 }
626
627 /** Removes task from visible tasks of all desks except [excludedDeskId]. */
628 private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) {
629 desktopData.forAllDesks { _, desk ->
630 if (desk.deskId != excludedDeskId) {
631 removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId)
632 }
633 }
634 }
635
636 private fun removeVisibleTaskFromDesk(deskId: Int, taskId: Int) {
637 val desk = desktopData.getDesk(deskId) ?: return
638 if (desk.visibleTasks.remove(taskId)) {
639 notifyVisibleTaskListeners(desk.displayId, desk.visibleTasks.size)
640 }
641 }
642
643 /**
644 * Updates visibility of a freeform task with [taskId] on [displayId] and notifies listeners.
645 *
646 * If task was visible on a different display with a different [displayId], removes from the set
647 * of visible tasks on that display and notifies listeners.
648 *
649 * TODO: b/389960283 - add explicit [deskId] argument.
650 */
651 fun updateTask(displayId: Int, taskId: Int, isVisible: Boolean) {
652 val validDisplayId =
653 if (displayId == INVALID_DISPLAY) {
654 // When a task vanishes it doesn't have a displayId. Find the display of the task.
655 getDisplayIdForTask(taskId)
656 } else {
657 displayId
658 }
659 if (validDisplayId == null) {
660 logW("No display id found for task: taskId=%d", taskId)
661 return
662 }
663 val desk =
664 checkNotNull(desktopData.getDefaultDesk(validDisplayId)) {
665 "Expected a desk in display: $validDisplayId"
666 }
667 updateTaskInDesk(
668 displayId = validDisplayId,
669 deskId = desk.deskId,
670 taskId = taskId,
671 isVisible,
672 )
673 }
674
675 private fun updateTaskInDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) {
676 check(displayId != INVALID_DISPLAY) { "Display must be valid" }
677 logD(
678 "updateTaskInDesk taskId=%d, deskId=%d, displayId=%d, isVisible=%b",
679 taskId,
680 deskId,
681 displayId,
682 isVisible,
683 )
684
685 if (isVisible) {
686 // If task is visible, remove it from any other desk besides [deskId].
687 removeVisibleTask(taskId, excludedDeskId = deskId)
688 }
689 val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
690 val prevCount = getVisibleTaskCountInDesk(deskId)
691 if (isVisible) {
692 desk.visibleTasks.add(taskId)
693 unminimizeTask(displayId, taskId)
694 } else {
695 desk.visibleTasks.remove(taskId)
696 }
697 val newCount = getVisibleTaskCountInDesk(deskId)
698 if (prevCount != newCount) {
699 logD(
700 "Update task visibility taskId=%d visible=%b deskId=%d displayId=%d",
701 taskId,
702 isVisible,
703 deskId,
704 displayId,
705 )
706 logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount)
707 notifyVisibleTaskListeners(displayId, newCount)
708 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
709 updatePersistentRepository(displayId)
710 }
711 }
712 }
713
714 /**
715 * Set whether the given task is the full-immersive task in this display's active desk.
716 *
717 * TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with
718 * an explicit desk id instead of using this function and defaulting to the active one.
719 */
720 fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
721 val activeDesk = desktopData.getActiveDesk(displayId) ?: return
722 setTaskInFullImmersiveStateInDesk(
723 deskId = activeDesk.deskId,
724 taskId = taskId,
725 immersive = immersive,
726 )
727 }
728
729 /** Sets whether the given task is the full-immersive task in the given desk. */
730 fun setTaskInFullImmersiveStateInDesk(deskId: Int, taskId: Int, immersive: Boolean) {
731 val desk = desktopData.getDesk(deskId) ?: return
732 if (immersive) {
733 desk.fullImmersiveTaskId = taskId
734 } else {
735 if (desk.fullImmersiveTaskId == taskId) {
736 desk.fullImmersiveTaskId = null
737 }
738 }
739 }
740
741 /* Whether the task is in full-immersive state. */
742 fun isTaskInFullImmersiveState(taskId: Int): Boolean {
743 return desksSequence().any { taskId == it.fullImmersiveTaskId }
744 }
745
746 /**
747 * Returns the task that is currently in immersive mode in this display, or null.
748 *
749 * TODO: b/389960283 - add explicit [deskId] argument.
750 */
751 fun getTaskInFullImmersiveState(displayId: Int): Int? =
752 desktopData.getActiveDesk(displayId)?.fullImmersiveTaskId
753
754 /**
755 * Sets the top transparent fullscreen task id for a given display's active desk.
756 *
757 * TODO: b/389960283 - add explicit [deskId] argument.
758 */
759 fun setTopTransparentFullscreenTaskId(displayId: Int, taskId: Int) {
760 logD(
761 "Top transparent fullscreen task set for display: taskId=%d, displayId=%d",
762 taskId,
763 displayId,
764 )
765 desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId = taskId
766 }
767
768 /**
769 * Returns the top transparent fullscreen task id for a given display, or null.
770 *
771 * TODO: b/389960283 - add explicit [deskId] argument.
772 */
773 fun getTopTransparentFullscreenTaskId(displayId: Int): Int? =
774 desktopData
775 .desksSequence(displayId)
776 .mapNotNull { it.topTransparentFullscreenTaskId }
777 .firstOrNull()
778
779 /**
780 * Clears the top transparent fullscreen task id info for a given display's active desk.
781 *
782 * TODO: b/389960283 - add explicit [deskId] argument.
783 */
784 fun clearTopTransparentFullscreenTaskId(displayId: Int) {
785 logD(
786 "Top transparent fullscreen task cleared for display: taskId=%d, displayId=%d",
787 desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId,
788 displayId,
789 )
790 desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId = null
791 }
792
793 @VisibleForTesting
794 public fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
795 visibleTasksListeners.forEach { (listener, executor) ->
796 executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
797 }
798 }
799
800 /** Whether the display is currently showing any desk. */
801 fun isAnyDeskActive(displayId: Int): Boolean {
802 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
803 val desk = desktopData.getDefaultDesk(displayId)
804 if (desk == null) {
805 logE("Could not find default desk for display: $displayId")
806 return false
807 }
808 return desk.visibleTasks.isNotEmpty()
809 }
810 return desktopData.getActiveDesk(displayId) != null
811 }
812
813 /** Gets number of visible freeform tasks on given [displayId]'s active desk. */
814 @Deprecated("Use isAnyDeskActive() instead.", ReplaceWith("isAnyDeskActive()"))
815 @VisibleForTesting
816 fun getVisibleTaskCount(displayId: Int): Int =
817 (desktopData.getActiveDesk(displayId)?.visibleTasks?.size ?: 0).also {
818 logD("getVisibleTaskCount=$it")
819 }
820
821 /** Gets the number of visible tasks on the given desk. */
822 private fun getVisibleTaskCountInDesk(deskId: Int): Int =
823 desktopData.getDesk(deskId)?.visibleTasks?.size ?: 0
824
825 /**
826 * Adds task (or moves if it already exists) to the top of the ordered list.
827 *
828 * Unminimizes the task if it is minimized.
829 */
830 private fun addOrMoveTaskToTopOfDesk(displayId: Int, deskId: Int, taskId: Int) {
831 logD(
832 "addOrMoveTaskToTopOfDesk displayId=%d, deskId=%d, taskId=%d",
833 displayId,
834 deskId,
835 taskId,
836 )
837 val desk = desktopData.getDesk(deskId) ?: error("Could not find desk: $deskId")
838 logD("addOrMoveTaskToTopOfDesk: display=%d deskId=%d taskId=%d", displayId, deskId, taskId)
839 desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) }
840 desk.freeformTasksInZOrder.add(0, taskId)
841 // TODO: double check minimization logic.
842 // Unminimize the task if it is minimized.
843 unminimizeTask(displayId, taskId)
844 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
845 // TODO: can probably just update the desk.
846 updatePersistentRepository(displayId)
847 }
848 }
849
850 /**
851 * Minimizes the task for [taskId] and [displayId]'s active display.
852 *
853 * TODO: b/389960283 - consider forcing callers to use [minimizeTaskInDesk] with an explicit
854 * desk id instead of using this function and defaulting to the active one.
855 */
856 fun minimizeTask(displayId: Int, taskId: Int) {
857 logD("minimizeTask displayId=%d, taskId=%d", displayId, taskId)
858 if (displayId == INVALID_DISPLAY) {
859 // When a task vanishes it doesn't have a displayId. Find the display of the task and
860 // mark it as minimized.
861 getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) }
862 ?: logW("Minimize task: No display id found for task: taskId=%d", taskId)
863 return
864 }
865 val deskId = desktopData.getActiveDesk(displayId)?.deskId
866 if (deskId == null) {
867 logD("Minimize task: No active desk found for task: taskId=%d", taskId)
868 return
869 }
870 minimizeTaskInDesk(displayId, deskId, taskId)
871 }
872
873 /** Minimizes the task in its desk. */
874 fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) {
875 logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId)
876 desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId)
877 ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId)
878 updateTaskInDesk(displayId, deskId, taskId, isVisible = false)
879 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
880 updatePersistentRepositoryForDesk(deskId)
881 }
882 }
883
884 /**
885 * Unminimizes the task for [taskId] and [displayId].
886 *
887 * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead.
888 */
889 fun unminimizeTask(displayId: Int, taskId: Int) {
890 logD("UnminimizeTask: display=%d, task=%d", displayId, taskId)
891 desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) }
892 }
893
894 private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) {
895 logD("Unminimize Task from desk: deskId=%d, taskId=%d", deskId, taskId)
896 if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) {
897 logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId)
898 }
899 }
900
901 private fun getDisplayIdForTask(taskId: Int): Int? {
902 var displayForTask: Int? = null
903 desktopData.forAllDesks { displayId, desk ->
904 if (taskId in desk.activeTasks) {
905 displayForTask = displayId
906 }
907 }
908 if (displayForTask == null) {
909 logW("No display id found for task: taskId=%d", taskId)
910 }
911 return displayForTask
912 }
913
914 /**
915 * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id
916 * will be looked up from the task id.
917 *
918 * TODO: b/389960283 - consider using [removeTaskFromDesk] instead.
919 */
920 fun removeTask(displayId: Int, taskId: Int) {
921 logD("Removes freeform task: taskId=%d", taskId)
922 if (displayId == INVALID_DISPLAY) {
923 // Removes the original display id of the task.
924 getDisplayIdForTask(taskId)?.let { removeTaskFromDisplay(it, taskId) }
925 } else {
926 removeTaskFromDisplay(displayId, taskId)
927 }
928 }
929
930 /** Removes given task from a valid [displayId] and updates the repository state. */
931 private fun removeTaskFromDisplay(displayId: Int, taskId: Int) {
932 logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId)
933 desktopData.forAllDesks(displayId) { desk ->
934 removeTaskFromDesk(deskId = desk.deskId, taskId = taskId)
935 }
936 }
937
938 /** Removes the given task from the given desk. */
939 fun removeTaskFromDesk(deskId: Int, taskId: Int) {
940 logD("removeTaskFromDesk: deskId=%d, taskId=%d", deskId, taskId)
941 // TODO: b/362720497 - consider not clearing bounds on any removal, such as when moving
942 // it between desks. It might be better to allow restoring to the previous bounds as long
943 // as they're valid (probably valid if in the same display).
944 boundsBeforeMaximizeByTaskId.remove(taskId)
945 boundsBeforeFullImmersiveByTaskId.remove(taskId)
946 val desk = desktopData.getDesk(deskId) ?: return
947 if (desk.freeformTasksInZOrder.remove(taskId)) {
948 logD(
949 "Remaining freeform tasks in desk: %d, tasks: %s",
950 desk.deskId,
951 desk.freeformTasksInZOrder.toDumpString(),
952 )
953 }
954 unminimizeTaskFromDesk(deskId, taskId)
955 // Mark task as not in immersive if it was immersive.
956 setTaskInFullImmersiveStateInDesk(deskId = deskId, taskId = taskId, immersive = false)
957 removeActiveTaskFromDesk(deskId = deskId, taskId = taskId)
958 removeVisibleTaskFromDesk(deskId = deskId, taskId = taskId)
959 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
960 updatePersistentRepositoryForDesk(desk.deskId)
961 }
962 }
963
964 /** Removes the given desk and returns the active tasks in that desk. */
965 fun removeDesk(deskId: Int): Set<Int> {
966 logD("removeDesk %d", deskId)
967 val desk =
968 desktopData.getDesk(deskId)
969 ?: return emptySet<Int>().also {
970 logW("Could not find desk to remove: deskId=%d", deskId)
971 }
972 val wasActive = desktopData.getActiveDesk(desk.displayId)?.deskId == desk.deskId
973 val activeTasks = ArraySet(desk.activeTasks)
974 desktopData.remove(desk.deskId)
975 notifyVisibleTaskListeners(desk.displayId, getVisibleTaskCount(displayId = desk.displayId))
976 deskChangeListeners.forEach { (listener, executor) ->
977 executor.execute {
978 if (wasActive) {
979 listener.onActiveDeskChanged(
980 displayId = desk.displayId,
981 newActiveDeskId = INVALID_DESK_ID,
982 oldActiveDeskId = desk.deskId,
983 )
984 }
985 listener.onDeskRemoved(displayId = desk.displayId, deskId = desk.deskId)
986 }
987 }
988 if (
989 DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue &&
990 DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
991 ) {
992 removeDeskFromPersistentRepository(desk)
993 }
994 return activeTasks
995 }
996
997 /**
998 * Updates active desktop gesture exclusion regions.
999 *
1000 * If [desktopExclusionRegions] is accepted by [desktopGestureExclusionListener], updates it in
1001 * appropriate classes.
1002 */
1003 fun updateTaskExclusionRegions(taskId: Int, taskExclusionRegions: Region) {
1004 desktopExclusionRegions.put(taskId, taskExclusionRegions)
1005 desktopGestureExclusionExecutor?.execute {
1006 desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
1007 }
1008 }
1009
1010 /**
1011 * Removes desktop gesture exclusion region for the specified task.
1012 *
1013 * If [desktopExclusionRegions] is accepted by [desktopGestureExclusionListener], updates it in
1014 * appropriate classes.
1015 */
1016 fun removeExclusionRegion(taskId: Int) {
1017 desktopExclusionRegions.delete(taskId)
1018 desktopGestureExclusionExecutor?.execute {
1019 desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
1020 }
1021 }
1022
1023 /** Removes and returns the bounds saved before maximizing the given task. */
1024 fun removeBoundsBeforeMaximize(taskId: Int): Rect? =
1025 boundsBeforeMaximizeByTaskId.removeReturnOld(taskId)
1026
1027 /** Saves the bounds of the given task before maximizing. */
1028 fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) =
1029 boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
1030
1031 /** Removes and returns the bounds saved before minimizing the given task. */
1032 fun removeBoundsBeforeMinimize(taskId: Int): Rect? =
1033 boundsBeforeMinimizeByTaskId.removeReturnOld(taskId)
1034
1035 /** Saves the bounds of the given task before minimizing. */
1036 fun saveBoundsBeforeMinimize(taskId: Int, bounds: Rect?) =
1037 boundsBeforeMinimizeByTaskId.set(taskId, Rect(bounds))
1038
1039 /** Removes and returns the bounds saved before entering immersive with the given task. */
1040 fun removeBoundsBeforeFullImmersive(taskId: Int): Rect? =
1041 boundsBeforeFullImmersiveByTaskId.removeReturnOld(taskId)
1042
1043 /** Saves the bounds of the given task before entering immersive. */
1044 fun saveBoundsBeforeFullImmersive(taskId: Int, bounds: Rect) =
1045 boundsBeforeFullImmersiveByTaskId.set(taskId, Rect(bounds))
1046
1047 /** Returns the current state of the desktop, formatted for usage by remote clients. */
1048 fun getDeskDisplayStateForRemote(): Array<DisplayDeskState> =
1049 desktopData
1050 .desksSequence()
1051 .groupBy { it.displayId }
1052 .map { (displayId, desks) ->
1053 val activeDeskId = desktopData.getActiveDesk(displayId)?.deskId
1054 DisplayDeskState().apply {
1055 this.displayId = displayId
1056 this.activeDeskId = activeDeskId ?: INVALID_DESK_ID
1057 this.deskIds = desks.map { it.deskId }.toIntArray()
1058 }
1059 }
1060 .toTypedArray()
1061
1062 /** TODO: b/389960283 - consider updating only the changing desks. */
1063 private fun updatePersistentRepository(displayId: Int) {
1064 val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList()
1065 mainCoroutineScope.launch {
1066 desks.forEach { desk -> updatePersistentRepositoryForDesk(desk) }
1067 }
1068 }
1069
1070 private fun updatePersistentRepositoryForDesk(deskId: Int) {
1071 val desk = desktopData.getDesk(deskId)?.deepCopy() ?: return
1072 mainCoroutineScope.launch { updatePersistentRepositoryForDesk(desk) }
1073 }
1074
1075 private suspend fun updatePersistentRepositoryForDesk(desk: Desk) {
1076 try {
1077 persistentRepository.addOrUpdateDesktop(
1078 userId = userId,
1079 desktopId = desk.deskId,
1080 visibleTasks = desk.visibleTasks,
1081 minimizedTasks = desk.minimizedTasks,
1082 freeformTasksInZOrder = desk.freeformTasksInZOrder,
1083 leftTiledTask = desk.leftTiledTaskId,
1084 rightTiledTask = desk.rightTiledTaskId,
1085 )
1086 } catch (exception: Exception) {
1087 logE(
1088 "An exception occurred while updating the persistent repository \n%s",
1089 exception.stackTrace,
1090 )
1091 }
1092 }
1093
1094 private fun removeDeskFromPersistentRepository(desk: Desk) {
1095 mainCoroutineScope.launch {
1096 try {
1097 logD(
1098 "updatePersistentRepositoryForRemovedDesk user=%d desk=%d",
1099 userId,
1100 desk.deskId,
1101 )
1102 persistentRepository.removeDesktop(userId = userId, desktopId = desk.deskId)
1103 } catch (throwable: Throwable) {
1104 logE(
1105 "An exception occurred while updating the persistent repository \n%s",
1106 throwable.stackTrace,
1107 )
1108 }
1109 }
1110 }
1111
1112 internal fun dump(pw: PrintWriter, prefix: String) {
1113 val innerPrefix = "$prefix "
1114 pw.println("${prefix}DesktopRepository")
1115 pw.println("${innerPrefix}userId=$userId")
1116 dumpDesktopTaskData(pw, innerPrefix)
1117 pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}")
1118 pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}")
1119 }
1120
1121 private fun dumpDesktopTaskData(pw: PrintWriter, prefix: String) {
1122 val innerPrefix = "$prefix "
1123 desktopData
1124 .desksSequence()
1125 .groupBy { it.displayId }
1126 .map { (displayId, desks) ->
1127 Triple(displayId, desktopData.getActiveDesk(displayId)?.deskId, desks)
1128 }
1129 .forEach { (displayId, activeDeskId, desks) ->
1130 pw.println("${prefix}Display #$displayId:")
1131 pw.println("${innerPrefix}numOfDesks=${desks.size}")
1132 pw.println("${innerPrefix}activeDesk=$activeDeskId")
1133 pw.println("${innerPrefix}desks:")
1134 val desksPrefix = "$innerPrefix "
1135 desks.forEach { desk ->
1136 pw.println("${desksPrefix}Desk #${desk.deskId}:")
1137 pw.print("$desksPrefix activeTasks=")
1138 pw.println(desk.activeTasks.toDumpString())
1139 pw.print("$desksPrefix visibleTasks=")
1140 pw.println(desk.visibleTasks.toDumpString())
1141 pw.print("$desksPrefix freeformTasksInZOrder=")
1142 pw.println(desk.freeformTasksInZOrder.toDumpString())
1143 pw.print("$desksPrefix minimizedTasks=")
1144 pw.println(desk.minimizedTasks.toDumpString())
1145 pw.print("$desksPrefix fullImmersiveTaskId=")
1146 pw.println(desk.fullImmersiveTaskId)
1147 pw.print("$desksPrefix topTransparentFullscreenTaskId=")
1148 pw.println(desk.topTransparentFullscreenTaskId)
1149 }
1150 }
1151 }
1152
1153 /** Listens to changes of desks state. */
1154 interface DeskChangeListener {
1155 /** Called when a new desk is added to a display. */
1156 fun onDeskAdded(displayId: Int, deskId: Int)
1157
1158 /** Called when a desk is removed from a display. */
1159 fun onDeskRemoved(displayId: Int, deskId: Int)
1160
1161 /** Called when the active desk in a display has changed. */
1162 fun onActiveDeskChanged(displayId: Int, newActiveDeskId: Int, oldActiveDeskId: Int)
1163 }
1164
1165 /** Listens to changes for active tasks in desktop mode. */
1166 interface ActiveTasksListener {
1167 fun onActiveTasksChanged(displayId: Int) {}
1168 }
1169
1170 /** Listens to changes for visible tasks in desktop mode. */
1171 interface VisibleTasksListener {
1172 fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {}
1173 }
1174
1175 /** An interface for the desktop hierarchy's data managed by this repository. */
1176 private interface DesktopData {
1177 /** Creates a desk record. */
1178 fun createDesk(displayId: Int, deskId: Int)
1179
1180 /** Returns the desk with the given id, or null if it does not exist. */
1181 fun getDesk(deskId: Int): Desk?
1182
1183 /** Returns the active desk in this diplay, or null if none are active. */
1184 fun getActiveDesk(displayId: Int): Desk?
1185
1186 /** Sets the given desk as the active desk in the given display. */
1187 fun setActiveDesk(displayId: Int, deskId: Int)
1188
1189 /** Sets the desk as inactive if it was active. */
1190 fun setDeskInactive(deskId: Int)
1191
1192 /**
1193 * Returns the default desk in the given display. Useful when the system wants to activate a
1194 * desk but doesn't care about which one it activates (e.g. when putting a window into a
1195 * desk using the App Handle). May return null if the display does not support desks.
1196 *
1197 * TODO: 389787966 - consider removing or renaming. In practice, this is needed for
1198 * soon-to-be deprecated IDesktopMode APIs, adb commands or entry-points into the only
1199 * desk (single-desk devices) or the most-recent desk (multi-desk devices).
1200 */
1201 fun getDefaultDesk(displayId: Int): Desk?
1202
1203 /** Returns all the active desks of all displays. */
1204 fun getAllActiveDesks(): Set<Desk>
1205
1206 /** Returns the number of desks in the given display. */
1207 fun getNumberOfDesks(displayId: Int): Int
1208
1209 /** Applies a function to all desks. */
1210 fun forAllDesks(consumer: (Desk) -> Unit)
1211
1212 /** Applies a function to all desks. */
1213 fun forAllDesks(consumer: (displayId: Int, Desk) -> Unit)
1214
1215 /** Applies a function to all desks under the given display. */
1216 fun forAllDesks(displayId: Int, consumer: (Desk) -> Unit)
1217
1218 /** Returns a sequence of all desks. */
1219 fun desksSequence(): Sequence<Desk>
1220
1221 /** Returns a sequence of all desks under the given display. */
1222 fun desksSequence(displayId: Int): Sequence<Desk>
1223
1224 /** Remove an existing desk if it exists. */
1225 fun remove(deskId: Int)
1226
1227 /** Returns the id of the display where the given desk is located. */
1228 fun getDisplayForDesk(deskId: Int): Int
1229 }
1230
1231 /**
1232 * A [DesktopData] implementation that only supports one desk per display.
1233 *
1234 * Internally, it reuses the displayId as that display's single desk's id. It also never truly
1235 * "removes" a desk, it just clears its content.
1236 */
1237 private class SingleDesktopData : DesktopData {
1238 private val deskByDisplayId =
1239 object : SparseArray<Desk>() {
1240 /** Gets [Desk] for existing [displayId] or creates a new one. */
1241 fun getOrCreate(displayId: Int): Desk =
1242 this[displayId]
1243 ?: Desk(deskId = displayId, displayId = displayId).also {
1244 this[displayId] = it
1245 }
1246 }
1247
1248 override fun createDesk(displayId: Int, deskId: Int) {
1249 check(displayId == deskId) { "Display and desk ids must match" }
1250 deskByDisplayId.getOrCreate(displayId)
1251 }
1252
1253 override fun getDesk(deskId: Int): Desk =
1254 // TODO: b/362720497 - consider enforcing that the desk has been created before trying
1255 // to use it. As of now, there are cases where a task may be created faster than a
1256 // desk is, so just create it here if needed. See b/391984373.
1257 deskByDisplayId.getOrCreate(deskId)
1258
1259 override fun getActiveDesk(displayId: Int): Desk {
1260 // TODO: 389787966 - consider migrating to an "active" state instead of checking the
1261 // number of visible active tasks, PIP in desktop, and empty desktop logic. In
1262 // practice, existing single-desktop devices are ok with this function returning the
1263 // only desktop, even if it's not active.
1264 return deskByDisplayId.getOrCreate(displayId)
1265 }
1266
1267 override fun setActiveDesk(displayId: Int, deskId: Int) {
1268 // No-op, in single-desk setups, which desktop is "active" is determined by the
1269 // existence of visible desktop windows, among other factors.
1270 }
1271
1272 override fun setDeskInactive(deskId: Int) {
1273 // No-op, in single-desk setups, which desktop is "active" is determined by the
1274 // existence of visible desktop windows, among other factors.
1275 }
1276
1277 override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId)
1278
1279 override fun getAllActiveDesks(): Set<Desk> =
1280 deskByDisplayId.valueIterator().asSequence().toSet()
1281
1282 override fun getNumberOfDesks(displayId: Int): Int = 1
1283
1284 override fun forAllDesks(consumer: (Desk) -> Unit) {
1285 deskByDisplayId.forEach { _, desk -> consumer(desk) }
1286 }
1287
1288 override fun forAllDesks(consumer: (Int, Desk) -> Unit) {
1289 deskByDisplayId.forEach { displayId, desk -> consumer(displayId, desk) }
1290 }
1291
1292 override fun forAllDesks(displayId: Int, consumer: (Desk) -> Unit) {
1293 consumer(getDesk(deskId = displayId))
1294 }
1295
1296 override fun desksSequence(): Sequence<Desk> = deskByDisplayId.valueIterator().asSequence()
1297
1298 override fun desksSequence(displayId: Int): Sequence<Desk> =
1299 deskByDisplayId[displayId]?.let { sequenceOf(it) } ?: emptySequence()
1300
1301 override fun remove(deskId: Int) {
1302 setDeskInactive(deskId)
1303 deskByDisplayId[deskId]?.clear()
1304 }
1305
1306 override fun getDisplayForDesk(deskId: Int): Int = deskId
1307 }
1308
1309 /** A [DesktopData] implementation that supports multiple desks. */
1310 private class MultiDesktopData : DesktopData {
1311 private val desktopDisplays = SparseArray<DesktopDisplay>()
1312
1313 override fun createDesk(displayId: Int, deskId: Int) {
1314 val display =
1315 desktopDisplays[displayId]
1316 ?: DesktopDisplay(displayId).also { desktopDisplays[displayId] = it }
1317 check(display.orderedDesks.none { desk -> desk.deskId == deskId }) {
1318 "Attempting to create desk#$deskId that already exists in display#$displayId"
1319 }
1320 display.orderedDesks.add(Desk(deskId = deskId, displayId = displayId))
1321 }
1322
1323 override fun getDesk(deskId: Int): Desk? {
1324 desktopDisplays.forEach { _, display ->
1325 val desk = display.orderedDesks.find { desk -> desk.deskId == deskId }
1326 if (desk != null) {
1327 return desk
1328 }
1329 }
1330 return null
1331 }
1332
1333 override fun getActiveDesk(displayId: Int): Desk? {
1334 val display = desktopDisplays[displayId] ?: return null
1335 if (display.activeDeskId == null) return null
1336 return display.orderedDesks.find { it.deskId == display.activeDeskId }
1337 }
1338
1339 override fun setActiveDesk(displayId: Int, deskId: Int) {
1340 val display =
1341 desktopDisplays[displayId] ?: error("Expected display#$displayId to exist")
1342 val desk = display.orderedDesks.single { it.deskId == deskId }
1343 display.activeDeskId = desk.deskId
1344 }
1345
1346 override fun setDeskInactive(deskId: Int) {
1347 desktopDisplays.forEach { id, display ->
1348 if (display.activeDeskId == deskId) {
1349 display.activeDeskId = null
1350 }
1351 }
1352 }
1353
1354 override fun getDefaultDesk(displayId: Int): Desk? {
1355 val display = desktopDisplays[displayId] ?: return null
1356 return display.orderedDesks.find { it.deskId == display.activeDeskId }
1357 ?: display.orderedDesks.firstOrNull()
1358 }
1359
1360 override fun getAllActiveDesks(): Set<Desk> {
1361 return desktopDisplays
1362 .valueIterator()
1363 .asSequence()
1364 .filter { display -> display.activeDeskId != null }
1365 .map { display ->
1366 display.orderedDesks.single { it.deskId == display.activeDeskId }
1367 }
1368 .toSet()
1369 }
1370
1371 override fun getNumberOfDesks(displayId: Int): Int =
1372 desktopDisplays[displayId]?.orderedDesks?.size ?: 0
1373
1374 override fun forAllDesks(consumer: (Desk) -> Unit) {
1375 desktopDisplays.forEach { _, display -> display.orderedDesks.forEach { consumer(it) } }
1376 }
1377
1378 override fun forAllDesks(consumer: (Int, Desk) -> Unit) {
1379 desktopDisplays.forEach { _, display ->
1380 display.orderedDesks.forEach { consumer(display.displayId, it) }
1381 }
1382 }
1383
1384 override fun forAllDesks(displayId: Int, consumer: (Desk) -> Unit) {
1385 desktopDisplays
1386 .valueIterator()
1387 .asSequence()
1388 .filter { display -> display.displayId == displayId }
1389 .flatMap { display -> display.orderedDesks.asSequence() }
1390 .forEach { desk -> consumer(desk) }
1391 }
1392
1393 override fun desksSequence(): Sequence<Desk> =
1394 desktopDisplays.valueIterator().asSequence().flatMap { display ->
1395 display.orderedDesks.asSequence()
1396 }
1397
1398 override fun desksSequence(displayId: Int): Sequence<Desk> =
1399 desktopDisplays[displayId]?.orderedDesks?.asSequence() ?: emptySequence()
1400
1401 override fun remove(deskId: Int) {
1402 setDeskInactive(deskId)
1403 desktopDisplays.forEach { _, display ->
1404 display.orderedDesks.removeIf { it.deskId == deskId }
1405 }
1406 }
1407
1408 override fun getDisplayForDesk(deskId: Int): Int =
1409 desksSequence().find { it.deskId == deskId }?.displayId
1410 ?: error("Display for desk=$deskId not found")
1411 }
1412
1413 private fun logD(msg: String, vararg arguments: Any?) {
1414 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1415 }
1416
1417 private fun logW(msg: String, vararg arguments: Any?) {
1418 ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1419 }
1420
1421 private fun logE(msg: String, vararg arguments: Any?) {
1422 ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1423 }
1424
1425 companion object {
1426 private const val TAG = "DesktopRepository"
1427
1428 @VisibleForTesting const val INVALID_DESK_ID = -1
1429 }
1430 }
1431
toDumpStringnull1432 private fun <T> Iterable<T>.toDumpString(): String =
1433 joinToString(separator = ", ", prefix = "[", postfix = "]")
1434