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