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