1 /* <lambda>null2 * Copyright (C) 2025 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 package com.android.wm.shell.desktopmode.multidesks 17 18 import android.annotation.SuppressLint 19 import android.app.ActivityManager.RunningTaskInfo 20 import android.app.ActivityTaskManager.INVALID_TASK_ID 21 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD 22 import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED 23 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 24 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED 25 import android.util.SparseArray 26 import android.view.SurfaceControl 27 import android.view.WindowManager.TRANSIT_TO_FRONT 28 import android.window.DesktopExperienceFlags 29 import android.window.TransitionInfo 30 import android.window.WindowContainerToken 31 import android.window.WindowContainerTransaction 32 import androidx.core.util.forEach 33 import androidx.core.util.valueIterator 34 import com.android.internal.annotations.VisibleForTesting 35 import com.android.internal.protolog.ProtoLog 36 import com.android.wm.shell.ShellTaskOrganizer 37 import com.android.wm.shell.common.LaunchAdjacentController 38 import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback 39 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 40 import com.android.wm.shell.sysui.ShellCommandHandler 41 import com.android.wm.shell.sysui.ShellInit 42 import java.io.PrintWriter 43 44 /** 45 * A [DesksOrganizer] that uses root tasks as the container of each desk. 46 * 47 * Note that root tasks are reusable between multiple users at the same time, and may also be 48 * pre-created to have one ready for the first entry to the default desk, so root-task existence 49 * does not imply a formal desk exists to the user. 50 */ 51 class RootTaskDesksOrganizer( 52 shellInit: ShellInit, 53 shellCommandHandler: ShellCommandHandler, 54 private val shellTaskOrganizer: ShellTaskOrganizer, 55 private val launchAdjacentController: LaunchAdjacentController, 56 ) : DesksOrganizer, ShellTaskOrganizer.TaskListener { 57 58 private val createDeskRootRequests = mutableListOf<CreateDeskRequest>() 59 @VisibleForTesting val deskRootsByDeskId = SparseArray<DeskRoot>() 60 private val createDeskMinimizationRootRequests = 61 mutableListOf<CreateDeskMinimizationRootRequest>() 62 @VisibleForTesting 63 val deskMinimizationRootsByDeskId: MutableMap<Int, DeskMinimizationRoot> = mutableMapOf() 64 private var onTaskInfoChangedListener: ((RunningTaskInfo) -> Unit)? = null 65 66 init { 67 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 68 shellInit.addInitCallback( 69 { shellCommandHandler.addDumpCallback(this::dump, this) }, 70 this, 71 ) 72 } 73 } 74 75 override fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback) { 76 logV("createDesk in displayId=%d userId=%s", displayId, userId) 77 // Find an existing desk that is not yet used by this user. 78 val unassignedDesk = 79 deskRootsByDeskId 80 .valueIterator() 81 .asSequence() 82 .filterNot { desk -> userId in desk.users } 83 .firstOrNull() 84 if (unassignedDesk != null) { 85 unassignedDesk.users.add(userId) 86 callback.onCreated(unassignedDesk.deskId) 87 return 88 } 89 createDeskRoot(displayId, userId, callback) 90 } 91 92 private fun createDeskRoot(displayId: Int, userId: Int, callback: OnCreateCallback) { 93 logV("createDeskRoot in display: %d for user: %d", displayId, userId) 94 createDeskRootRequests += CreateDeskRequest(displayId, userId, callback) 95 shellTaskOrganizer.createRootTask( 96 displayId, 97 WINDOWING_MODE_FREEFORM, 98 /* listener = */ this, 99 /* removeWithTaskOrganizer = */ true, 100 ) 101 } 102 103 override fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int) { 104 logV("removeDesk %d for userId=%d", deskId, userId) 105 val deskRoot = deskRootsByDeskId[deskId] 106 if (deskRoot == null) { 107 logW("removeDesk attempted to remove non-existent desk=%d", deskId) 108 return 109 } 110 updateLaunchRoot(wct, deskId, enabled = false) 111 deskRoot.users.remove(userId) 112 if (deskRoot.users.isEmpty()) { 113 // No longer in use by any users, remove it completely. 114 logD("removeDesk %d is no longer used by any users, removing it completely", deskId) 115 wct.removeRootTask(deskRoot.token) 116 deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) } 117 } 118 } 119 120 override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) { 121 logV("activateDesk %d", deskId) 122 val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } 123 wct.reorder(root.token, /* onTop= */ true) 124 updateLaunchRoot(wct, deskId, enabled = true) 125 } 126 127 override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) { 128 logV("deactivateDesk %d", deskId) 129 updateLaunchRoot(wct, deskId, enabled = false) 130 } 131 132 private fun updateLaunchRoot(wct: WindowContainerTransaction, deskId: Int, enabled: Boolean) { 133 val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } 134 root.isLaunchRootRequested = enabled 135 logD("updateLaunchRoot deskId=%d enabled=%b", deskId, enabled) 136 if (enabled) { 137 wct.setLaunchRoot( 138 /* container= */ root.taskInfo.token, 139 /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), 140 /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD), 141 ) 142 } else { 143 wct.setLaunchRoot( 144 /* container= */ root.taskInfo.token, 145 /* windowingModes= */ null, 146 /* activityTypes= */ null, 147 ) 148 } 149 } 150 151 override fun moveTaskToDesk( 152 wct: WindowContainerTransaction, 153 deskId: Int, 154 task: RunningTaskInfo, 155 ) { 156 val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId") 157 wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) 158 wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) 159 } 160 161 override fun reorderTaskToFront( 162 wct: WindowContainerTransaction, 163 deskId: Int, 164 task: RunningTaskInfo, 165 ) { 166 logV("reorderTaskToFront task=${task.taskId} desk=$deskId") 167 val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId") 168 if (task.taskId in root.children) { 169 wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) 170 return 171 } 172 val minimizationRoot = 173 checkNotNull(deskMinimizationRootsByDeskId[deskId]) { 174 "Minimization root not found for desk: $deskId" 175 } 176 if (task.taskId in minimizationRoot.children) { 177 unminimizeTask(wct, deskId, task) 178 wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) 179 return 180 } 181 logE("Attempted to reorder task=${task.taskId} in desk=$deskId but it was not a child") 182 } 183 184 override fun minimizeTask(wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo) { 185 logV("minimizeTask task=${task.taskId} desk=$deskId") 186 val deskRoot = 187 checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } 188 val minimizationRoot = 189 checkNotNull(deskMinimizationRootsByDeskId[deskId]) { 190 "Minimization root not found for desk: $deskId" 191 } 192 val taskId = task.taskId 193 if (taskId in minimizationRoot.children) { 194 logV("Task #$taskId is already minimized in desk #$deskId") 195 return 196 } 197 if (taskId !in deskRoot.children) { 198 logE("Attempted to minimize task=${task.taskId} in desk=$deskId but it was not a child") 199 return 200 } 201 wct.reparent(task.token, minimizationRoot.token, /* onTop= */ true) 202 } 203 204 override fun unminimizeTask( 205 wct: WindowContainerTransaction, 206 deskId: Int, 207 task: RunningTaskInfo, 208 ) { 209 val taskId = task.taskId 210 logV("unminimizeTask task=$taskId desk=$deskId") 211 val deskRoot = 212 checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } 213 val minimizationRoot = 214 checkNotNull(deskMinimizationRootsByDeskId[deskId]) { 215 "Minimization root not found for desk: $deskId" 216 } 217 if (taskId in deskRoot.children) { 218 logV("Task #$taskId is already unminimized in desk=$deskId") 219 return 220 } 221 if (taskId !in minimizationRoot.children) { 222 logE("Attempted to unminimize task=$taskId in desk=$deskId but it was not a child") 223 return 224 } 225 wct.reparent(task.token, deskRoot.token, /* onTop= */ true) 226 } 227 228 override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean = 229 (isDeskRootChange(change) && change.taskId == deskId) || 230 (getDeskMinimizationRootInChange(change)?.deskId == deskId) 231 232 override fun isDeskChange(change: TransitionInfo.Change): Boolean = 233 isDeskRootChange(change) || getDeskMinimizationRootInChange(change) != null 234 235 private fun isDeskRootChange(change: TransitionInfo.Change): Boolean = 236 change.taskId in deskRootsByDeskId 237 238 private fun getDeskMinimizationRootInChange( 239 change: TransitionInfo.Change 240 ): DeskMinimizationRoot? = 241 deskMinimizationRootsByDeskId.values.find { it.rootId == change.taskId } 242 243 private val TransitionInfo.Change.taskId: Int 244 get() = taskInfo?.taskId ?: INVALID_TASK_ID 245 246 override fun getDeskAtEnd(change: TransitionInfo.Change): Int? { 247 val parentTaskId = change.taskInfo?.parentTaskId ?: return null 248 if (parentTaskId in deskRootsByDeskId) { 249 return parentTaskId 250 } 251 val deskMinimizationRoot = 252 deskMinimizationRootsByDeskId.values.find { root -> root.rootId == parentTaskId } 253 ?: return null 254 return deskMinimizationRoot.deskId 255 } 256 257 override fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean = 258 change.taskInfo?.taskId == deskId && 259 change.taskInfo?.isVisibleRequested == true && 260 change.mode == TRANSIT_TO_FRONT 261 262 override fun setOnDesktopTaskInfoChangedListener(listener: (RunningTaskInfo) -> Unit) { 263 onTaskInfoChangedListener = listener 264 } 265 266 override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { 267 handleTaskAppeared(taskInfo, leash) 268 updateLaunchAdjacentController() 269 } 270 271 override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { 272 handleTaskInfoChanged(taskInfo) 273 if ( 274 taskInfo.taskId !in deskRootsByDeskId && 275 deskMinimizationRootsByDeskId.values.none { it.rootId == taskInfo.taskId } 276 ) { 277 onTaskInfoChangedListener?.invoke(taskInfo) 278 } 279 updateLaunchAdjacentController() 280 } 281 282 override fun onTaskVanished(taskInfo: RunningTaskInfo) { 283 handleTaskVanished(taskInfo) 284 updateLaunchAdjacentController() 285 } 286 287 private fun handleTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { 288 // Check whether this task is appearing inside a desk. 289 if (taskInfo.parentTaskId in deskRootsByDeskId) { 290 val deskId = taskInfo.parentTaskId 291 val taskId = taskInfo.taskId 292 logV("Task #$taskId appeared in desk #$deskId") 293 addChildToDesk(taskId = taskId, deskId = deskId) 294 return 295 } 296 // Check whether this task is appearing in a minimization root. 297 val minimizationRoot = 298 deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.parentTaskId } 299 if (minimizationRoot != null) { 300 val deskId = minimizationRoot.deskId 301 val taskId = taskInfo.taskId 302 logV("Task #$taskId was minimized in desk #$deskId ") 303 addChildToMinimizationRoot(taskId = taskId, deskId = deskId) 304 return 305 } 306 // The appearing task is a root (either a desk or a minimization root), it should not exist 307 // already. 308 check(taskInfo.taskId !in deskRootsByDeskId) { 309 "A root already exists for desk: ${taskInfo.taskId}" 310 } 311 check(deskMinimizationRootsByDeskId.values.none { it.rootId == taskInfo.taskId }) { 312 "A minimization root already exists with rootId: ${taskInfo.taskId}" 313 } 314 315 val appearingInDisplayId = taskInfo.displayId 316 // Check if there's any pending desk creation requests under this display. 317 val deskRequest = 318 createDeskRootRequests.firstOrNull { it.displayId == appearingInDisplayId } 319 if (deskRequest != null) { 320 // Appearing root matches desk request. 321 val deskId = taskInfo.taskId 322 logV("Desk #$deskId appeared") 323 deskRootsByDeskId[deskId] = 324 DeskRoot( 325 deskId = deskId, 326 taskInfo = taskInfo, 327 leash = leash, 328 users = mutableSetOf(deskRequest.userId), 329 ) 330 createDeskRootRequests.remove(deskRequest) 331 deskRequest.onCreateCallback.onCreated(deskId) 332 createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId) 333 return 334 } 335 // Check if there's any pending minimization container creation requests under this display. 336 val deskMinimizationRootRequest = 337 createDeskMinimizationRootRequests.first { it.displayId == appearingInDisplayId } 338 val deskId = deskMinimizationRootRequest.deskId 339 logV("Minimization container for desk #$deskId appeared with id=${taskInfo.taskId}") 340 val deskMinimizationRoot = DeskMinimizationRoot(deskId, taskInfo, leash) 341 deskMinimizationRootsByDeskId[deskId] = deskMinimizationRoot 342 createDeskMinimizationRootRequests.remove(deskMinimizationRootRequest) 343 hideMinimizationRoot(deskMinimizationRoot) 344 } 345 346 private fun handleTaskInfoChanged(taskInfo: RunningTaskInfo) { 347 if (deskRootsByDeskId.contains(taskInfo.taskId)) { 348 val deskId = taskInfo.taskId 349 deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo) 350 logV("Desk #$deskId's task info changed") 351 return 352 } 353 val minimizationRoot = 354 deskMinimizationRootsByDeskId.values.find { root -> root.rootId == taskInfo.taskId } 355 if (minimizationRoot != null) { 356 deskMinimizationRootsByDeskId.remove(minimizationRoot.deskId) 357 deskMinimizationRootsByDeskId[minimizationRoot.deskId] = 358 minimizationRoot.copy(taskInfo = taskInfo) 359 logV("Minimization root for desk#${minimizationRoot.deskId} task info changed") 360 return 361 } 362 363 val parentTaskId = taskInfo.parentTaskId 364 if (parentTaskId in deskRootsByDeskId) { 365 val deskId = taskInfo.parentTaskId 366 val taskId = taskInfo.taskId 367 logV("onTaskInfoChanged: Task #$taskId appeared in desk #$deskId") 368 addChildToDesk(taskId = taskId, deskId = deskId) 369 return 370 } 371 // Check whether this task is appearing in a minimization root. 372 val parentMinimizationRoot = 373 deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == parentTaskId } 374 if (parentMinimizationRoot != null) { 375 val deskId = parentMinimizationRoot.deskId 376 val taskId = taskInfo.taskId 377 logV("onTaskInfoChanged: Task #$taskId was minimized in desk #$deskId ") 378 addChildToMinimizationRoot(taskId = taskId, deskId = deskId) 379 return 380 } 381 logE("onTaskInfoChanged: unknown task: ${taskInfo.taskId}") 382 } 383 384 private fun handleTaskVanished(taskInfo: RunningTaskInfo) { 385 if (deskRootsByDeskId.contains(taskInfo.taskId)) { 386 val deskId = taskInfo.taskId 387 val deskRoot = deskRootsByDeskId[deskId] 388 // Use the last saved taskInfo to obtain the displayId. Using the local one here will 389 // return -1 since the task is not unassociated with a display. 390 val displayId = deskRoot.taskInfo.displayId 391 logV("Desk #$deskId vanished from display #$displayId") 392 deskRootsByDeskId.remove(deskId) 393 return 394 } 395 val deskMinimizationRoot = 396 deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.taskId } 397 if (deskMinimizationRoot != null) { 398 logV("Minimization root for desk ${deskMinimizationRoot.deskId} vanished") 399 deskMinimizationRootsByDeskId.remove(deskMinimizationRoot.deskId) 400 return 401 } 402 403 // Check whether the vanishing task was a child of any desk. 404 // At this point, [parentTaskId] may be unset even if this is a task vanishing from a desk, 405 // so search through each root to remove this if it's a child. 406 deskRootsByDeskId.forEach { deskId, deskRoot -> 407 if (deskRoot.children.remove(taskInfo.taskId)) { 408 logV("Task #${taskInfo.taskId} vanished from desk #$deskId") 409 return 410 } 411 } 412 // Check whether the vanishing task was a child of the minimized root and remove it. 413 deskMinimizationRootsByDeskId.values.forEach { root -> 414 val taskId = taskInfo.taskId 415 if (root.children.remove(taskId)) { 416 logV("Task #$taskId vanished from minimization root of desk #${root.deskId}") 417 return 418 } 419 } 420 } 421 422 private fun createDeskMinimizationRoot(displayId: Int, deskId: Int) { 423 createDeskMinimizationRootRequests += 424 CreateDeskMinimizationRootRequest(displayId = displayId, deskId = deskId) 425 shellTaskOrganizer.createRootTask( 426 displayId, 427 WINDOWING_MODE_FREEFORM, 428 /* listener = */ this, 429 /* removeWithTaskOrganizer = */ true, 430 ) 431 } 432 433 @SuppressLint("MissingPermission") 434 private fun hideMinimizationRoot(root: DeskMinimizationRoot) { 435 shellTaskOrganizer.applyTransaction( 436 WindowContainerTransaction().apply { setHidden(root.token, /* hidden= */ true) } 437 ) 438 } 439 440 private fun addChildToDesk(taskId: Int, deskId: Int) { 441 deskRootsByDeskId.forEach { _, deskRoot -> 442 if (deskRoot.deskId == deskId) { 443 deskRoot.children.add(taskId) 444 } else { 445 deskRoot.children.remove(taskId) 446 } 447 } 448 // A task cannot be in both a desk root and a minimization root at the same time, so make 449 // sure to remove them if needed. 450 deskMinimizationRootsByDeskId.values.forEach { root -> root.children.remove(taskId) } 451 } 452 453 private fun addChildToMinimizationRoot(taskId: Int, deskId: Int) { 454 deskMinimizationRootsByDeskId.forEach { _, minimizationRoot -> 455 if (minimizationRoot.deskId == deskId) { 456 minimizationRoot.children += taskId 457 } else { 458 minimizationRoot.children -= taskId 459 } 460 } 461 // A task cannot be in both a desk root and a minimization root at the same time, so make 462 // sure to remove them if needed. 463 deskRootsByDeskId.forEach { _, deskRoot -> deskRoot.children -= taskId } 464 } 465 466 private fun updateLaunchAdjacentController() { 467 deskRootsByDeskId.forEach { deskId, root -> 468 if (root.taskInfo.isVisible) { 469 // Disable launch adjacent handling if any desk is active, otherwise the split 470 // launch root and the desk root will both be eligible to take launching tasks. 471 launchAdjacentController.launchAdjacentEnabled = false 472 return 473 } 474 } 475 launchAdjacentController.launchAdjacentEnabled = true 476 } 477 478 @VisibleForTesting 479 data class DeskRoot( 480 val deskId: Int, 481 val taskInfo: RunningTaskInfo, 482 val leash: SurfaceControl, 483 val children: MutableSet<Int> = mutableSetOf(), 484 val users: MutableSet<Int> = mutableSetOf(), 485 var isLaunchRootRequested: Boolean = false, 486 ) { 487 val token: WindowContainerToken = taskInfo.token 488 } 489 490 @VisibleForTesting 491 data class DeskMinimizationRoot( 492 val deskId: Int, 493 val taskInfo: RunningTaskInfo, 494 val leash: SurfaceControl, 495 val children: MutableSet<Int> = mutableSetOf(), 496 ) { 497 val rootId: Int 498 get() = taskInfo.taskId 499 500 val token: WindowContainerToken = taskInfo.token 501 } 502 503 private data class CreateDeskRequest( 504 val displayId: Int, 505 val userId: Int, 506 val onCreateCallback: OnCreateCallback, 507 ) 508 509 private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int) 510 511 private fun logD(msg: String, vararg arguments: Any?) { 512 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 513 } 514 515 private fun logV(msg: String, vararg arguments: Any?) { 516 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 517 } 518 519 private fun logW(msg: String, vararg arguments: Any?) { 520 ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 521 } 522 523 private fun logE(msg: String, vararg arguments: Any?) { 524 ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 525 } 526 527 override fun dump(pw: PrintWriter, prefix: String) { 528 val innerPrefix = "$prefix " 529 pw.println("$prefix$TAG") 530 pw.println( 531 "${innerPrefix}launchAdjacentEnabled=" + launchAdjacentController.launchAdjacentEnabled 532 ) 533 pw.println("${innerPrefix}Desk Roots:") 534 deskRootsByDeskId.forEach { deskId, root -> 535 val minimizationRoot = deskMinimizationRootsByDeskId[deskId] 536 pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}") 537 pw.println("$innerPrefix displayId=${root.taskInfo.displayId}") 538 pw.println("$innerPrefix isLaunchRootRequested=${root.isLaunchRootRequested}") 539 pw.println("$innerPrefix children=${root.children}") 540 pw.println("$innerPrefix users=${root.users}") 541 pw.println("$innerPrefix minimization root:") 542 pw.println("$innerPrefix rootId=${minimizationRoot?.rootId}") 543 if (minimizationRoot != null) { 544 pw.println("$innerPrefix children=${minimizationRoot.children}") 545 } 546 } 547 } 548 549 companion object { 550 private const val TAG = "RootTaskDesksOrganizer" 551 } 552 } 553