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 }