• 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.WindowContainerToken
26 import androidx.core.util.forEach
27 import androidx.core.util.keyIterator
28 import androidx.core.util.valueIterator
29 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
30 import com.android.wm.shell.util.KtProtoLog
31 import java.io.PrintWriter
32 import java.util.concurrent.Executor
33 import java.util.function.Consumer
34 
35 /** Keeps track of task data related to desktop mode. */
36 class DesktopModeTaskRepository {
37 
38     /** Task data that is tracked per display */
39     private data class DisplayData(
40         /**
41          * Set of task ids that are marked as active in desktop mode. Active tasks in desktop mode
42          * are freeform tasks that are visible or have been visible after desktop mode was
43          * activated. Task gets removed from this list when it vanishes. Or when desktop mode is
44          * turned off.
45          */
46         val activeTasks: ArraySet<Int> = ArraySet(),
47         val visibleTasks: ArraySet<Int> = ArraySet(),
48         val minimizedTasks: ArraySet<Int> = ArraySet(),
49         // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
50         val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
51     )
52 
53     // Token of the current wallpaper activity, used to remove it when the last task is removed
54     var wallpaperActivityToken: WindowContainerToken? = null
55     private val activeTasksListeners = ArraySet<ActiveTasksListener>()
56     // Track visible tasks separately because a task may be part of the desktop but not visible.
57     private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
58     // Track corner/caption regions of desktop tasks, used to determine gesture exclusion
59     private val desktopExclusionRegions = SparseArray<Region>()
60     // Track last bounds of task before toggled to stable bounds
61     private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>()
62     private var desktopGestureExclusionListener: Consumer<Region>? = null
63     private var desktopGestureExclusionExecutor: Executor? = null
64 
65     private val displayData =
66         object : SparseArray<DisplayData>() {
67             /**
68              * Get the [DisplayData] associated with this [displayId]
69              *
70              * Creates a new instance if one does not exist
71              */
72             fun getOrCreate(displayId: Int): DisplayData {
73                 if (!contains(displayId)) {
74                     put(displayId, DisplayData())
75                 }
76                 return get(displayId)
77             }
78         }
79 
80     /** Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository. */
81     fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) {
82         activeTasksListeners.add(activeTasksListener)
83     }
84 
85     /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */
86     fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
87         visibleTasksListeners[visibleTasksListener] = executor
88         displayData.keyIterator().forEach { displayId ->
89             val visibleTasksCount = getVisibleTaskCount(displayId)
90             executor.execute {
91                 visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount)
92             }
93         }
94     }
95 
96     /**
97      * Add a Consumer which will inform other classes of changes to exclusion regions for all
98      * Desktop tasks.
99      */
100     fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) {
101         desktopGestureExclusionListener = regionListener
102         desktopGestureExclusionExecutor = executor
103         executor.execute {
104             desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
105         }
106     }
107 
108     /** Create a new merged region representative of all exclusion regions in all desktop tasks. */
109     private fun calculateDesktopExclusionRegion(): Region {
110         val desktopExclusionRegion = Region()
111         desktopExclusionRegions.valueIterator().forEach { taskExclusionRegion ->
112             desktopExclusionRegion.op(taskExclusionRegion, Region.Op.UNION)
113         }
114         return desktopExclusionRegion
115     }
116 
117     /** Remove a previously registered [ActiveTasksListener] */
118     fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
119         activeTasksListeners.remove(activeTasksListener)
120     }
121 
122     /** Remove a previously registered [VisibleTasksListener] */
123     fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
124         visibleTasksListeners.remove(visibleTasksListener)
125     }
126 
127     /**
128      * Mark a task with given [taskId] as active on given [displayId]
129      *
130      * @return `true` if the task was not active on given [displayId]
131      */
132     fun addActiveTask(displayId: Int, taskId: Int): Boolean {
133         // Check if task is active on another display, if so, remove it
134         displayData.forEach { id, data ->
135             if (id != displayId && data.activeTasks.remove(taskId)) {
136                 activeTasksListeners.onEach { it.onActiveTasksChanged(id) }
137             }
138         }
139 
140         val added = displayData.getOrCreate(displayId).activeTasks.add(taskId)
141         if (added) {
142             KtProtoLog.d(
143                 WM_SHELL_DESKTOP_MODE,
144                 "DesktopTaskRepo: add active task=%d displayId=%d",
145                 taskId,
146                 displayId
147             )
148             activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) }
149         }
150         return added
151     }
152 
153     /**
154      * Remove task with given [taskId] from active tasks.
155      *
156      * @return `true` if the task was active
157      */
158     fun removeActiveTask(taskId: Int): Boolean {
159         var result = false
160         displayData.forEach { displayId, data ->
161             if (data.activeTasks.remove(taskId)) {
162                 activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) }
163                 result = true
164             }
165         }
166         if (result) {
167             KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove active task=%d", taskId)
168         }
169         return result
170     }
171 
172     /** Check if a task with the given [taskId] was marked as an active task */
173     fun isActiveTask(taskId: Int): Boolean {
174         return displayData.valueIterator().asSequence().any { data ->
175             data.activeTasks.contains(taskId)
176         }
177     }
178 
179     /** Whether a task is visible. */
180     fun isVisibleTask(taskId: Int): Boolean {
181         return displayData.valueIterator().asSequence().any { data ->
182             data.visibleTasks.contains(taskId)
183         }
184     }
185 
186     /** Return whether the given Task is minimized. */
187     fun isMinimizedTask(taskId: Int): Boolean {
188         return displayData.valueIterator().asSequence().any { data ->
189             data.minimizedTasks.contains(taskId)
190         }
191     }
192 
193     /** Check if a task with the given [taskId] is the only active task on its display */
194     fun isOnlyActiveTask(taskId: Int): Boolean {
195         return displayData.valueIterator().asSequence().any { data ->
196             data.activeTasks.singleOrNull() == taskId
197         }
198     }
199 
200     /** Get a set of the active tasks for given [displayId] */
201     fun getActiveTasks(displayId: Int): ArraySet<Int> {
202         return ArraySet(displayData[displayId]?.activeTasks)
203     }
204 
205     /**
206      * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks
207      * are visible.
208      */
209     fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0
210 
211     /**
212      * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display,
213      * ordered from front to back.
214      */
215     fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> {
216         val activeTasks = getActiveTasks(displayId)
217         val allTasksInZOrder = getFreeformTasksInZOrder(displayId)
218         return activeTasks
219             // Don't show already minimized Tasks
220             .filter { taskId -> !isMinimizedTask(taskId) }
221             .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) }
222     }
223 
224     /** Get a list of freeform tasks, ordered from top-bottom (top at index 0). */
225     fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> =
226         ArrayList(displayData[displayId]?.freeformTasksInZOrder ?: emptyList())
227 
228     /**
229      * Updates whether a freeform task with this id is visible or not and notifies listeners.
230      *
231      * If the task was visible on a different display with a different displayId, it is removed from
232      * the set of visible tasks on that display. Listeners will be notified.
233      */
234     fun updateVisibleFreeformTasks(displayId: Int, taskId: Int, visible: Boolean) {
235         if (visible) {
236             // Task is visible. Check if we need to remove it from any other display.
237             val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId }
238             for (otherDisplayId in otherDisplays) {
239                 if (displayData[otherDisplayId].visibleTasks.remove(taskId)) {
240                     notifyVisibleTaskListeners(
241                         otherDisplayId,
242                         displayData[otherDisplayId].visibleTasks.size
243                     )
244                 }
245             }
246         } else if (displayId == INVALID_DISPLAY) {
247             // Task has vanished. Check which display to remove the task from.
248             displayData.forEach { displayId, data ->
249                 if (data.visibleTasks.remove(taskId)) {
250                     notifyVisibleTaskListeners(displayId, data.visibleTasks.size)
251                 }
252             }
253             return
254         }
255 
256         val prevCount = getVisibleTaskCount(displayId)
257         if (visible) {
258             displayData.getOrCreate(displayId).visibleTasks.add(taskId)
259             unminimizeTask(displayId, taskId)
260         } else {
261             displayData[displayId]?.visibleTasks?.remove(taskId)
262         }
263         val newCount = getVisibleTaskCount(displayId)
264 
265         // Check if count changed
266         if (prevCount != newCount) {
267             KtProtoLog.d(
268                 WM_SHELL_DESKTOP_MODE,
269                 "DesktopTaskRepo: update task visibility taskId=%d visible=%b displayId=%d",
270                 taskId,
271                 visible,
272                 displayId
273             )
274             KtProtoLog.d(
275                 WM_SHELL_DESKTOP_MODE,
276                 "DesktopTaskRepo: visibleTaskCount has changed from %d to %d",
277                 prevCount,
278                 newCount
279             )
280             notifyVisibleTaskListeners(displayId, newCount)
281         }
282     }
283 
284     private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
285         visibleTasksListeners.forEach { (listener, executor) ->
286             executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
287         }
288     }
289 
290     /** Get number of tasks that are marked as visible on given [displayId] */
291     fun getVisibleTaskCount(displayId: Int): Int {
292         KtProtoLog.d(
293             WM_SHELL_DESKTOP_MODE,
294             "DesktopTaskRepo: visibleTaskCount= %d",
295             displayData[displayId]?.visibleTasks?.size ?: 0
296         )
297         return displayData[displayId]?.visibleTasks?.size ?: 0
298     }
299 
300     /** Add (or move if it already exists) the task to the top of the ordered list. */
301     // TODO(b/342417921): Identify if there is additional checks needed to move tasks for
302     // multi-display scenarios.
303     fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) {
304         KtProtoLog.d(
305             WM_SHELL_DESKTOP_MODE,
306             "DesktopTaskRepo: add or move task to top: display=%d, taskId=%d",
307             displayId,
308             taskId
309         )
310         displayData[displayId]?.freeformTasksInZOrder?.remove(taskId)
311         displayData.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId)
312     }
313 
314     /** Mark a Task as minimized. */
315     fun minimizeTask(displayId: Int, taskId: Int) {
316         KtProtoLog.v(
317             WM_SHELL_DESKTOP_MODE,
318             "DesktopModeTaskRepository: minimize Task: display=%d, task=%d",
319             displayId,
320             taskId
321         )
322         displayData.getOrCreate(displayId).minimizedTasks.add(taskId)
323     }
324 
325     /** Mark a Task as non-minimized. */
326     fun unminimizeTask(displayId: Int, taskId: Int) {
327         KtProtoLog.v(
328             WM_SHELL_DESKTOP_MODE,
329             "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d",
330             displayId,
331             taskId
332         )
333         displayData[displayId]?.minimizedTasks?.remove(taskId)
334     }
335 
336     /** Remove the task from the ordered list. */
337     fun removeFreeformTask(displayId: Int, taskId: Int) {
338         KtProtoLog.d(
339             WM_SHELL_DESKTOP_MODE,
340             "DesktopTaskRepo: remove freeform task from ordered list: display=%d, taskId=%d",
341             displayId,
342             taskId
343         )
344         displayData[displayId]?.freeformTasksInZOrder?.remove(taskId)
345         boundsBeforeMaximizeByTaskId.remove(taskId)
346         KtProtoLog.d(
347             WM_SHELL_DESKTOP_MODE,
348             "DesktopTaskRepo: remaining freeform tasks: %s",
349             displayData[displayId]?.freeformTasksInZOrder?.toDumpString() ?: ""
350         )
351     }
352 
353     /**
354      * Updates the active desktop gesture exclusion regions; if desktopExclusionRegions has been
355      * accepted by desktopGestureExclusionListener, it will be updated in the appropriate classes.
356      */
357     fun updateTaskExclusionRegions(taskId: Int, taskExclusionRegions: Region) {
358         desktopExclusionRegions.put(taskId, taskExclusionRegions)
359         desktopGestureExclusionExecutor?.execute {
360             desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
361         }
362     }
363 
364     /**
365      * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion has
366      * been accepted by desktopGestureExclusionListener, it will be updated in the appropriate
367      * classes.
368      */
369     fun removeExclusionRegion(taskId: Int) {
370         desktopExclusionRegions.delete(taskId)
371         desktopGestureExclusionExecutor?.execute {
372             desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
373         }
374     }
375 
376     /** Removes and returns the bounds saved before maximizing the given task. */
377     fun removeBoundsBeforeMaximize(taskId: Int): Rect? {
378         return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId)
379     }
380 
381     /** Saves the bounds of the given task before maximizing. */
382     fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) {
383         boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
384     }
385 
386     internal fun dump(pw: PrintWriter, prefix: String) {
387         val innerPrefix = "$prefix  "
388         pw.println("${prefix}DesktopModeTaskRepository")
389         dumpDisplayData(pw, innerPrefix)
390         pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}")
391         pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}")
392     }
393 
394     private fun dumpDisplayData(pw: PrintWriter, prefix: String) {
395         val innerPrefix = "$prefix  "
396         displayData.forEach { displayId, data ->
397             pw.println("${prefix}Display $displayId:")
398             pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}")
399             pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}")
400             pw.println(
401                 "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}"
402             )
403         }
404     }
405 
406     /**
407      * Defines interface for classes that can listen to changes for active tasks in desktop mode.
408      */
409     interface ActiveTasksListener {
410         /** Called when the active tasks change in desktop mode. */
411         fun onActiveTasksChanged(displayId: Int) {}
412     }
413 
414     /**
415      * Defines interface for classes that can listen to changes for visible tasks in desktop mode.
416      */
417     interface VisibleTasksListener {
418         /** Called when the desktop changes the number of visible freeform tasks. */
419         fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {}
420     }
421 }
422 
toDumpStringnull423 private fun <T> Iterable<T>.toDumpString(): String {
424     return joinToString(separator = ", ", prefix = "[", postfix = "]")
425 }
426