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.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
toDumpStringnull393 private fun <T> Iterable<T>.toDumpString(): String {
394 return joinToString(separator = ", ", prefix = "[", postfix = "]")
395 }