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