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.app.ActivityManager.RunningTaskInfo 20 import android.app.ActivityOptions 21 import android.app.PendingIntent 22 import android.app.TaskInfo 23 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME 24 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD 25 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 26 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN 27 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW 28 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED 29 import android.app.WindowConfiguration.WindowingMode 30 import android.content.Context 31 import android.content.Intent 32 import android.graphics.Point 33 import android.graphics.PointF 34 import android.graphics.Rect 35 import android.graphics.Region 36 import android.os.IBinder 37 import android.os.SystemProperties 38 import android.view.Display.DEFAULT_DISPLAY 39 import android.view.SurfaceControl 40 import android.view.WindowManager.TRANSIT_CHANGE 41 import android.view.WindowManager.TRANSIT_NONE 42 import android.view.WindowManager.TRANSIT_OPEN 43 import android.view.WindowManager.TRANSIT_TO_BACK 44 import android.view.WindowManager.TRANSIT_TO_FRONT 45 import android.window.RemoteTransition 46 import android.window.TransitionInfo 47 import android.window.TransitionRequestInfo 48 import android.window.WindowContainerTransaction 49 import androidx.annotation.BinderThread 50 import com.android.internal.annotations.VisibleForTesting 51 import com.android.internal.policy.ScreenDecorationsUtils 52 import com.android.window.flags.Flags 53 import com.android.wm.shell.RootTaskDisplayAreaOrganizer 54 import com.android.wm.shell.ShellTaskOrganizer 55 import com.android.wm.shell.common.DisplayController 56 import com.android.wm.shell.common.DisplayLayout 57 import com.android.wm.shell.common.ExternalInterfaceBinder 58 import com.android.wm.shell.common.LaunchAdjacentController 59 import com.android.wm.shell.common.MultiInstanceHelper 60 import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent 61 import com.android.wm.shell.common.RemoteCallable 62 import com.android.wm.shell.common.ShellExecutor 63 import com.android.wm.shell.common.SingleInstanceRemoteListener 64 import com.android.wm.shell.common.SyncTransactionQueue 65 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource 66 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT 67 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT 68 import com.android.wm.shell.compatui.isSingleTopActivityTranslucent 69 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener 70 import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener 71 import com.android.wm.shell.draganddrop.DragAndDropController 72 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 73 import com.android.wm.shell.recents.RecentTasksController 74 import com.android.wm.shell.recents.RecentsTransitionHandler 75 import com.android.wm.shell.recents.RecentsTransitionStateListener 76 import com.android.wm.shell.shared.DesktopModeStatus 77 import com.android.wm.shell.shared.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE 78 import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity 79 import com.android.wm.shell.shared.annotations.ExternalThread 80 import com.android.wm.shell.shared.annotations.ShellMainThread 81 import com.android.wm.shell.splitscreen.SplitScreenController 82 import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE 83 import com.android.wm.shell.sysui.ShellCommandHandler 84 import com.android.wm.shell.sysui.ShellController 85 import com.android.wm.shell.sysui.ShellInit 86 import com.android.wm.shell.sysui.ShellSharedConstants 87 import com.android.wm.shell.transition.OneShotRemoteHandler 88 import com.android.wm.shell.transition.Transitions 89 import com.android.wm.shell.util.KtProtoLog 90 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility 91 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator 92 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener 93 import com.android.wm.shell.windowdecor.extension.isFullscreen 94 import java.io.PrintWriter 95 import java.util.Optional 96 import java.util.concurrent.Executor 97 import java.util.function.Consumer 98 99 /** Handles moving tasks in and out of desktop */ 100 class DesktopTasksController( 101 private val context: Context, 102 shellInit: ShellInit, 103 private val shellCommandHandler: ShellCommandHandler, 104 private val shellController: ShellController, 105 private val displayController: DisplayController, 106 private val shellTaskOrganizer: ShellTaskOrganizer, 107 private val syncQueue: SyncTransactionQueue, 108 private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, 109 private val dragAndDropController: DragAndDropController, 110 private val transitions: Transitions, 111 private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, 112 private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, 113 private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, 114 private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, 115 private val desktopModeTaskRepository: DesktopModeTaskRepository, 116 private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver, 117 private val launchAdjacentController: LaunchAdjacentController, 118 private val recentsTransitionHandler: RecentsTransitionHandler, 119 private val multiInstanceHelper: MultiInstanceHelper, 120 @ShellMainThread private val mainExecutor: ShellExecutor, 121 private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, 122 private val recentTasksController: RecentTasksController? 123 ) : 124 RemoteCallable<DesktopTasksController>, 125 Transitions.TransitionHandler, 126 DragAndDropController.DragAndDropListener { 127 128 private val desktopMode: DesktopModeImpl 129 private var visualIndicator: DesktopModeVisualIndicator? = null 130 private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = 131 DesktopModeShellCommandHandler(this) 132 private val mOnAnimationFinishedCallback = 133 Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction -> 134 visualIndicator?.releaseVisualIndicator(t) 135 visualIndicator = null 136 } 137 private val taskVisibilityListener = 138 object : VisibleTasksListener { 139 override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { 140 launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0 141 } 142 } 143 private val dragToDesktopStateListener = 144 object : DragToDesktopStateListener { 145 override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) { 146 removeVisualIndicator(tx) 147 } 148 149 override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) { 150 removeVisualIndicator(tx) 151 } 152 153 private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { 154 visualIndicator?.releaseVisualIndicator(tx) 155 visualIndicator = null 156 } 157 } 158 private val sysUIPackageName = context.resources.getString( 159 com.android.internal.R.string.config_systemUi) 160 161 private val transitionAreaHeight 162 get() = 163 context.resources.getDimensionPixelSize( 164 com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height 165 ) 166 167 private val transitionAreaWidth 168 get() = 169 context.resources.getDimensionPixelSize( 170 com.android.wm.shell.R.dimen.desktop_mode_transition_area_width 171 ) 172 173 /** Task id of the task currently being dragged from fullscreen/split. */ 174 val draggingTaskId 175 get() = dragToDesktopTransitionHandler.draggingTaskId 176 177 private var recentsAnimationRunning = false 178 private lateinit var splitScreenController: SplitScreenController 179 180 init { 181 desktopMode = DesktopModeImpl() 182 if (DesktopModeStatus.canEnterDesktopMode(context)) { 183 shellInit.addInitCallback({ onInit() }, this) 184 } 185 } 186 187 private fun onInit() { 188 KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") 189 shellCommandHandler.addDumpCallback(this::dump, this) 190 shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, this) 191 shellController.addExternalInterface( 192 ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, 193 { createExternalInterface() }, 194 this 195 ) 196 transitions.addHandler(this) 197 desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor) 198 dragToDesktopTransitionHandler.setDragToDesktopStateListener(dragToDesktopStateListener) 199 recentsTransitionHandler.addTransitionStateListener( 200 object : RecentsTransitionStateListener { 201 override fun onAnimationStateChanged(running: Boolean) { 202 KtProtoLog.v( 203 WM_SHELL_DESKTOP_MODE, 204 "DesktopTasksController: recents animation state changed running=%b", 205 running 206 ) 207 recentsAnimationRunning = running 208 } 209 } 210 ) 211 dragAndDropController.addListener(this) 212 } 213 214 @VisibleForTesting 215 fun getVisualIndicator(): DesktopModeVisualIndicator? { 216 return visualIndicator 217 } 218 219 // TODO(b/347289970): Consider replacing with API 220 private fun isSystemUIApplication(taskInfo: RunningTaskInfo): Boolean { 221 return taskInfo.baseActivity?.packageName == sysUIPackageName 222 } 223 224 fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { 225 toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) 226 enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) 227 dragToDesktopTransitionHandler.setOnTaskResizeAnimatorListener(listener) 228 } 229 230 /** Setter needed to avoid cyclic dependency. */ 231 fun setSplitScreenController(controller: SplitScreenController) { 232 splitScreenController = controller 233 dragToDesktopTransitionHandler.setSplitScreenController(controller) 234 } 235 236 /** Show all tasks, that are part of the desktop, on top of launcher */ 237 fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) { 238 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps") 239 val wct = WindowContainerTransaction() 240 bringDesktopAppsToFront(displayId, wct) 241 242 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 243 // TODO(b/309014605): ensure remote transition is supplied once state is introduced 244 val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT 245 val handler = 246 remoteTransition?.let { 247 OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) 248 } 249 transitions.startTransition(transitionType, wct, handler).also { t -> 250 handler?.setTransition(t) 251 } 252 } else { 253 shellTaskOrganizer.applyTransaction(wct) 254 } 255 } 256 257 /** Get number of tasks that are marked as visible */ 258 fun getVisibleTaskCount(displayId: Int): Int { 259 return desktopModeTaskRepository.getVisibleTaskCount(displayId) 260 } 261 262 /** Enter desktop by using the focused task in given `displayId` */ 263 fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) { 264 val allFocusedTasks = 265 shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> 266 taskInfo.isFocused && 267 (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || 268 taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) && 269 taskInfo.activityType != ACTIVITY_TYPE_HOME 270 } 271 if (allFocusedTasks.isNotEmpty()) { 272 when (allFocusedTasks.size) { 273 2 -> { 274 // Split-screen case where there are two focused tasks, then we find the child 275 // task to move to desktop. 276 val splitFocusedTask = 277 if (allFocusedTasks[0].taskId == allFocusedTasks[1].parentTaskId) { 278 allFocusedTasks[1] 279 } else { 280 allFocusedTasks[0] 281 } 282 moveToDesktop(splitFocusedTask, transitionSource = transitionSource) 283 } 284 1 -> { 285 // Fullscreen case where we move the current focused task. 286 moveToDesktop(allFocusedTasks[0].taskId, transitionSource = transitionSource) 287 } 288 else -> { 289 KtProtoLog.w( 290 WM_SHELL_DESKTOP_MODE, 291 "DesktopTasksController: Cannot enter desktop, expected less " + 292 "than 3 focused tasks but found %d", 293 allFocusedTasks.size 294 ) 295 } 296 } 297 } 298 } 299 300 /** Move a task with given `taskId` to desktop */ 301 fun moveToDesktop( 302 taskId: Int, 303 wct: WindowContainerTransaction = WindowContainerTransaction(), 304 transitionSource: DesktopModeTransitionSource, 305 ): Boolean { 306 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { 307 moveToDesktop(it, wct, transitionSource) 308 } 309 ?: moveToDesktopFromNonRunningTask(taskId, wct, transitionSource) 310 return true 311 } 312 313 private fun moveToDesktopFromNonRunningTask( 314 taskId: Int, 315 wct: WindowContainerTransaction, 316 transitionSource: DesktopModeTransitionSource, 317 ): Boolean { 318 recentTasksController?.findTaskInBackground(taskId)?.let { 319 KtProtoLog.v( 320 WM_SHELL_DESKTOP_MODE, 321 "DesktopTasksController: moveToDesktopFromNonRunningTask taskId=%d", 322 taskId 323 ) 324 // TODO(342378842): Instead of using default display, support multiple displays 325 val taskToMinimize = 326 bringDesktopAppsToFrontBeforeShowingNewTask(DEFAULT_DISPLAY, wct, taskId) 327 addMoveToDesktopChangesNonRunningTask(wct, taskId) 328 // TODO(343149901): Add DPI changes for task launch 329 val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) 330 addPendingMinimizeTransition(transition, taskToMinimize) 331 return true 332 } 333 ?: return false 334 } 335 336 private fun addMoveToDesktopChangesNonRunningTask( 337 wct: WindowContainerTransaction, 338 taskId: Int 339 ) { 340 val options = ActivityOptions.makeBasic() 341 options.launchWindowingMode = WINDOWING_MODE_FREEFORM 342 wct.startTask(taskId, options.toBundle()) 343 } 344 345 /** Move a task to desktop */ 346 fun moveToDesktop( 347 task: RunningTaskInfo, 348 wct: WindowContainerTransaction = WindowContainerTransaction(), 349 transitionSource: DesktopModeTransitionSource, 350 ) { 351 if (Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)) { 352 KtProtoLog.w( 353 WM_SHELL_DESKTOP_MODE, 354 "DesktopTasksController: Cannot enter desktop, " + 355 "translucent top activity found. This is likely a modal dialog." 356 ) 357 return 358 } 359 if (isSystemUIApplication(task)) { 360 KtProtoLog.w( 361 WM_SHELL_DESKTOP_MODE, 362 "DesktopTasksController: Cannot enter desktop, " + 363 "systemUI top activity found." 364 ) 365 return 366 } 367 KtProtoLog.v( 368 WM_SHELL_DESKTOP_MODE, 369 "DesktopTasksController: moveToDesktop taskId=%d", 370 task.taskId 371 ) 372 exitSplitIfApplicable(wct, task) 373 // Bring other apps to front first 374 val taskToMinimize = 375 bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) 376 addMoveToDesktopChanges(wct, task) 377 378 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 379 val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) 380 addPendingMinimizeTransition(transition, taskToMinimize) 381 } else { 382 shellTaskOrganizer.applyTransaction(wct) 383 } 384 } 385 386 /** 387 * The first part of the animated drag to desktop transition. This is followed with a call to 388 * [finalizeDragToDesktop] or [cancelDragToDesktop]. 389 */ 390 fun startDragToDesktop( 391 taskInfo: RunningTaskInfo, 392 dragToDesktopValueAnimator: MoveToDesktopAnimator, 393 ) { 394 KtProtoLog.v( 395 WM_SHELL_DESKTOP_MODE, 396 "DesktopTasksController: startDragToDesktop taskId=%d", 397 taskInfo.taskId 398 ) 399 dragToDesktopTransitionHandler.startDragToDesktopTransition( 400 taskInfo.taskId, 401 dragToDesktopValueAnimator 402 ) 403 } 404 405 /** 406 * The second part of the animated drag to desktop transition, called after 407 * [startDragToDesktop]. 408 */ 409 private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) { 410 KtProtoLog.v( 411 WM_SHELL_DESKTOP_MODE, 412 "DesktopTasksController: finalizeDragToDesktop taskId=%d", 413 taskInfo.taskId 414 ) 415 val wct = WindowContainerTransaction() 416 exitSplitIfApplicable(wct, taskInfo) 417 moveHomeTaskToFront(wct) 418 val taskToMinimize = 419 bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId) 420 addMoveToDesktopChanges(wct, taskInfo) 421 wct.setBounds(taskInfo.token, freeformBounds) 422 val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) 423 transition?.let { addPendingMinimizeTransition(it, taskToMinimize) } 424 } 425 426 /** 427 * Perform needed cleanup transaction once animation is complete. Bounds need to be set here 428 * instead of initial wct to both avoid flicker and to have task bounds to use for the staging 429 * animation. 430 * 431 * @param taskInfo task entering split that requires a bounds update 432 */ 433 fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { 434 val wct = WindowContainerTransaction() 435 wct.setBounds(taskInfo.token, Rect()) 436 wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED) 437 shellTaskOrganizer.applyTransaction(wct) 438 } 439 440 /** 441 * Perform clean up of the desktop wallpaper activity if the closed window task is the last 442 * active task. 443 * 444 * @param wct transaction to modify if the last active task is closed 445 * @param taskId task id of the window that's being closed 446 */ 447 fun onDesktopWindowClose(wct: WindowContainerTransaction, taskId: Int) { 448 if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) { 449 removeWallpaperActivity(wct) 450 } 451 } 452 453 /** Move a task with given `taskId` to fullscreen */ 454 fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) { 455 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> 456 moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource) 457 } 458 } 459 460 /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */ 461 fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) { 462 getFocusedFreeformTask(displayId)?.let { 463 moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource) 464 } 465 } 466 467 /** Move a desktop app to split screen. */ 468 fun moveToSplit(task: RunningTaskInfo) { 469 KtProtoLog.v( 470 WM_SHELL_DESKTOP_MODE, 471 "DesktopTasksController: moveToSplit taskId=%d", 472 task.taskId 473 ) 474 val wct = WindowContainerTransaction() 475 wct.setBounds(task.token, Rect()) 476 // Rather than set windowing mode to multi-window at task level, set it to 477 // undefined and inherit from split stage. 478 wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) 479 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 480 transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) 481 } else { 482 shellTaskOrganizer.applyTransaction(wct) 483 } 484 } 485 486 private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { 487 if (splitScreenController.isTaskInSplitScreen(taskInfo.taskId)) { 488 splitScreenController.prepareExitSplitScreen( 489 wct, 490 splitScreenController.getStageOfTask(taskInfo.taskId), 491 EXIT_REASON_DESKTOP_MODE 492 ) 493 splitScreenController.transitionHandler?.onSplitToDesktop() 494 } 495 } 496 497 /** 498 * The second part of the animated drag to desktop transition, called after 499 * [startDragToDesktop]. 500 */ 501 fun cancelDragToDesktop(task: RunningTaskInfo) { 502 KtProtoLog.v( 503 WM_SHELL_DESKTOP_MODE, 504 "DesktopTasksController: cancelDragToDesktop taskId=%d", 505 task.taskId 506 ) 507 dragToDesktopTransitionHandler.cancelDragToDesktopTransition( 508 DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL 509 ) 510 } 511 512 private fun moveToFullscreenWithAnimation( 513 task: RunningTaskInfo, 514 position: Point, 515 transitionSource: DesktopModeTransitionSource 516 ) { 517 KtProtoLog.v( 518 WM_SHELL_DESKTOP_MODE, 519 "DesktopTasksController: moveToFullscreen with animation taskId=%d", 520 task.taskId 521 ) 522 val wct = WindowContainerTransaction() 523 addMoveToFullscreenChanges(wct, task) 524 525 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 526 exitDesktopTaskTransitionHandler.startTransition( 527 transitionSource, 528 wct, 529 position, 530 mOnAnimationFinishedCallback 531 ) 532 } else { 533 shellTaskOrganizer.applyTransaction(wct) 534 releaseVisualIndicator() 535 } 536 } 537 538 /** Move a task to the front */ 539 fun moveTaskToFront(taskId: Int) { 540 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) } 541 } 542 543 /** Move a task to the front */ 544 fun moveTaskToFront(taskInfo: RunningTaskInfo) { 545 KtProtoLog.v( 546 WM_SHELL_DESKTOP_MODE, 547 "DesktopTasksController: moveTaskToFront taskId=%d", 548 taskInfo.taskId 549 ) 550 551 val wct = WindowContainerTransaction() 552 wct.reorder(taskInfo.token, true) 553 val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo) 554 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 555 val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) 556 addPendingMinimizeTransition(transition, taskToMinimize) 557 } else { 558 shellTaskOrganizer.applyTransaction(wct) 559 } 560 } 561 562 /** 563 * Move task to the next display. 564 * 565 * Queries all current known display ids and sorts them in ascending order. Then iterates 566 * through the list and looks for the display id that is larger than the display id for the 567 * passed in task. If a display with a higher id is not found, iterates through the list and 568 * finds the first display id that is not the display id for the passed in task. 569 * 570 * If a display matching the above criteria is found, re-parents the task to that display. No-op 571 * if no such display is found. 572 */ 573 fun moveToNextDisplay(taskId: Int) { 574 val task = shellTaskOrganizer.getRunningTaskInfo(taskId) 575 if (task == null) { 576 KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId) 577 return 578 } 579 KtProtoLog.v( 580 WM_SHELL_DESKTOP_MODE, 581 "moveToNextDisplay: taskId=%d taskDisplayId=%d", 582 taskId, 583 task.displayId 584 ) 585 586 val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted() 587 // Get the first display id that is higher than current task display id 588 var newDisplayId = displayIds.firstOrNull { displayId -> displayId > task.displayId } 589 if (newDisplayId == null) { 590 // No display with a higher id, get the first display id that is not the task display id 591 newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId } 592 } 593 if (newDisplayId == null) { 594 KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found") 595 return 596 } 597 moveToDisplay(task, newDisplayId) 598 } 599 600 /** 601 * Move [task] to display with [displayId]. 602 * 603 * No-op if task is already on that display per [RunningTaskInfo.displayId]. 604 */ 605 private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) { 606 KtProtoLog.v( 607 WM_SHELL_DESKTOP_MODE, 608 "moveToDisplay: taskId=%d displayId=%d", 609 task.taskId, 610 displayId 611 ) 612 613 if (task.displayId == displayId) { 614 KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display") 615 return 616 } 617 618 val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) 619 if (displayAreaInfo == null) { 620 KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found") 621 return 622 } 623 624 val wct = WindowContainerTransaction() 625 wct.reparent(task.token, displayAreaInfo.token, true /* onTop */) 626 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 627 transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) 628 } else { 629 shellTaskOrganizer.applyTransaction(wct) 630 } 631 } 632 633 /** 634 * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable 635 * bounds) and a free floating state (either the last saved bounds if available or the default 636 * bounds otherwise). 637 */ 638 fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) { 639 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return 640 641 val stableBounds = Rect() 642 displayLayout.getStableBounds(stableBounds) 643 val destinationBounds = Rect() 644 if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) { 645 // The desktop task is currently occupying the whole stable bounds. If the bounds 646 // before the task was toggled to stable bounds were saved, toggle the task to those 647 // bounds. Otherwise, toggle to the default bounds. 648 val taskBoundsBeforeMaximize = 649 desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId) 650 if (taskBoundsBeforeMaximize != null) { 651 destinationBounds.set(taskBoundsBeforeMaximize) 652 } else { 653 if (Flags.enableWindowingDynamicInitialBounds()) { 654 destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo)) 655 } else { 656 destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout)) 657 } 658 } 659 } else { 660 // Save current bounds so that task can be restored back to original bounds if necessary 661 // and toggle to the stable bounds. 662 val taskBounds = taskInfo.configuration.windowConfiguration.bounds 663 desktopModeTaskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, taskBounds) 664 destinationBounds.set(stableBounds) 665 } 666 667 val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) 668 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 669 toggleResizeDesktopTaskTransitionHandler.startTransition(wct) 670 } else { 671 shellTaskOrganizer.applyTransaction(wct) 672 } 673 } 674 675 /** 676 * Quick-resize to the right or left half of the stable bounds. 677 * 678 * @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to. 679 */ 680 fun snapToHalfScreen(taskInfo: RunningTaskInfo, position: SnapPosition) { 681 val destinationBounds = getSnapBounds(taskInfo, position) 682 683 if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return 684 685 val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) 686 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 687 toggleResizeDesktopTaskTransitionHandler.startTransition(wct) 688 } else { 689 shellTaskOrganizer.applyTransaction(wct) 690 } 691 } 692 693 private fun getDefaultDesktopTaskBounds(displayLayout: DisplayLayout): Rect { 694 // TODO(b/319819547): Account for app constraints so apps do not become letterboxed 695 val desiredWidth = (displayLayout.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt() 696 val desiredHeight = (displayLayout.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt() 697 val heightOffset = (displayLayout.height() - desiredHeight) / 2 698 val widthOffset = (displayLayout.width() - desiredWidth) / 2 699 return Rect( 700 widthOffset, 701 heightOffset, 702 desiredWidth + widthOffset, 703 desiredHeight + heightOffset 704 ) 705 } 706 707 private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect { 708 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect() 709 710 val stableBounds = Rect() 711 displayLayout.getStableBounds(stableBounds) 712 713 val destinationWidth = stableBounds.width() / 2 714 return when (position) { 715 SnapPosition.LEFT -> { 716 Rect( 717 stableBounds.left, 718 stableBounds.top, 719 stableBounds.left + destinationWidth, 720 stableBounds.bottom 721 ) 722 } 723 SnapPosition.RIGHT -> { 724 Rect( 725 stableBounds.right - destinationWidth, 726 stableBounds.top, 727 stableBounds.right, 728 stableBounds.bottom 729 ) 730 } 731 } 732 } 733 734 /** 735 * Get windowing move for a given `taskId` 736 * 737 * @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found 738 */ 739 @WindowingMode 740 fun getTaskWindowingMode(taskId: Int): Int { 741 return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode 742 ?: WINDOWING_MODE_UNDEFINED 743 } 744 745 private fun bringDesktopAppsToFrontBeforeShowingNewTask( 746 displayId: Int, 747 wct: WindowContainerTransaction, 748 newTaskIdInFront: Int 749 ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront) 750 751 private fun bringDesktopAppsToFront( 752 displayId: Int, 753 wct: WindowContainerTransaction, 754 newTaskIdInFront: Int? = null 755 ): RunningTaskInfo? { 756 KtProtoLog.v( 757 WM_SHELL_DESKTOP_MODE, 758 "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s", 759 newTaskIdInFront ?: "null" 760 ) 761 762 if (Flags.enableDesktopWindowingWallpaperActivity()) { 763 // Add translucent wallpaper activity to show the wallpaper underneath 764 addWallpaperActivity(wct) 765 } else { 766 // Move home to front 767 moveHomeTaskToFront(wct) 768 } 769 770 val nonMinimizedTasksOrderedFrontToBack = 771 desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId) 772 // If we're adding a new Task we might need to minimize an old one 773 val taskToMinimize: RunningTaskInfo? = 774 if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { 775 desktopTasksLimiter 776 .get() 777 .getTaskToMinimizeIfNeeded( 778 nonMinimizedTasksOrderedFrontToBack, 779 newTaskIdInFront 780 ) 781 } else { 782 null 783 } 784 nonMinimizedTasksOrderedFrontToBack 785 // If there is a Task to minimize, let it stay behind the Home Task 786 .filter { taskId -> taskId != taskToMinimize?.taskId } 787 .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } 788 .reversed() // Start from the back so the front task is brought forward last 789 .forEach { task -> wct.reorder(task.token, true /* onTop */) } 790 return taskToMinimize 791 } 792 793 private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { 794 shellTaskOrganizer 795 .getRunningTasks(context.displayId) 796 .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } 797 ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) } 798 } 799 800 private fun addWallpaperActivity(wct: WindowContainerTransaction) { 801 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: addWallpaper") 802 val intent = Intent(context, DesktopWallpaperActivity::class.java) 803 val options = 804 ActivityOptions.makeBasic().apply { 805 isPendingIntentBackgroundActivityLaunchAllowedByPermission = true 806 pendingIntentBackgroundActivityStartMode = 807 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED 808 } 809 val pendingIntent = 810 PendingIntent.getActivity( 811 context, 812 /* requestCode = */ 0, 813 intent, 814 PendingIntent.FLAG_IMMUTABLE 815 ) 816 wct.sendPendingIntent(pendingIntent, intent, options.toBundle()) 817 } 818 819 private fun removeWallpaperActivity(wct: WindowContainerTransaction) { 820 desktopModeTaskRepository.wallpaperActivityToken?.let { token -> 821 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: removeWallpaper") 822 wct.removeTask(token) 823 } 824 } 825 826 fun releaseVisualIndicator() { 827 val t = SurfaceControl.Transaction() 828 visualIndicator?.releaseVisualIndicator(t) 829 visualIndicator = null 830 syncQueue.runInSync { transaction -> 831 transaction.merge(t) 832 t.close() 833 } 834 } 835 836 override fun getContext(): Context { 837 return context 838 } 839 840 override fun getRemoteCallExecutor(): ShellExecutor { 841 return mainExecutor 842 } 843 844 override fun startAnimation( 845 transition: IBinder, 846 info: TransitionInfo, 847 startTransaction: SurfaceControl.Transaction, 848 finishTransaction: SurfaceControl.Transaction, 849 finishCallback: Transitions.TransitionFinishCallback 850 ): Boolean { 851 // This handler should never be the sole handler, so should not animate anything. 852 return false 853 } 854 855 override fun handleRequest( 856 transition: IBinder, 857 request: TransitionRequestInfo 858 ): WindowContainerTransaction? { 859 KtProtoLog.v( 860 WM_SHELL_DESKTOP_MODE, 861 "DesktopTasksController: handleRequest request=%s", 862 request 863 ) 864 // Check if we should skip handling this transition 865 var reason = "" 866 val triggerTask = request.triggerTask 867 val shouldHandleRequest = 868 when { 869 recentsAnimationRunning -> { 870 reason = "recents animation is running" 871 false 872 } 873 // Handle back navigation for the last window if wallpaper available 874 shouldRemoveWallpaper(request) -> true 875 // Only handle open or to front transitions 876 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> { 877 reason = "transition type not handled (${request.type})" 878 false 879 } 880 // Only handle when it is a task transition 881 triggerTask == null -> { 882 reason = "triggerTask is null" 883 false 884 } 885 // Only handle standard type tasks 886 triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> { 887 reason = "activityType not handled (${triggerTask.activityType})" 888 false 889 } 890 // Only handle fullscreen or freeform tasks 891 triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN && 892 triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> { 893 reason = "windowingMode not handled (${triggerTask.windowingMode})" 894 false 895 } 896 // Otherwise process it 897 else -> true 898 } 899 900 if (!shouldHandleRequest) { 901 KtProtoLog.v( 902 WM_SHELL_DESKTOP_MODE, 903 "DesktopTasksController: skipping handleRequest reason=%s", 904 reason 905 ) 906 return null 907 } 908 909 val result = 910 triggerTask?.let { task -> 911 when { 912 request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) 913 // Check if the task has a top transparent activity 914 shouldLaunchAsModal(task) -> handleIncompatibleTaskLaunch(task) 915 // Check if the task has a top systemUI activity 916 isSystemUIApplication(task) -> handleIncompatibleTaskLaunch(task) 917 // Check if fullscreen task should be updated 918 task.isFullscreen -> handleFullscreenTaskLaunch(task, transition) 919 // Check if freeform task should be updated 920 task.isFreeform -> handleFreeformTaskLaunch(task, transition) 921 else -> { 922 null 923 } 924 } 925 } 926 KtProtoLog.v( 927 WM_SHELL_DESKTOP_MODE, 928 "DesktopTasksController: handleRequest result=%s", 929 result ?: "null" 930 ) 931 return result 932 } 933 934 /** 935 * Applies the proper surface states (rounded corners) to tasks when desktop mode is active. 936 * This is intended to be used when desktop mode is part of another animation but isn't, itself, 937 * animating. 938 */ 939 fun syncSurfaceState(info: TransitionInfo, finishTransaction: SurfaceControl.Transaction) { 940 // Add rounded corners to freeform windows 941 if (!DesktopModeStatus.useRoundedCorners()) { 942 return 943 } 944 val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) 945 info.changes 946 .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM } 947 .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } 948 } 949 950 // TODO(b/347289970): Consider replacing with API 951 private fun shouldLaunchAsModal(task: TaskInfo) = 952 Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task) 953 954 private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean { 955 return Flags.enableDesktopWindowingWallpaperActivity() && 956 request.type == TRANSIT_TO_BACK && 957 request.triggerTask?.let { task -> 958 desktopModeTaskRepository.isOnlyActiveTask(task.taskId) 959 } 960 ?: false 961 } 962 963 private fun handleFreeformTaskLaunch( 964 task: RunningTaskInfo, 965 transition: IBinder 966 ): WindowContainerTransaction? { 967 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") 968 if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { 969 KtProtoLog.d( 970 WM_SHELL_DESKTOP_MODE, 971 "DesktopTasksController: switch freeform task to fullscreen oon transition" + 972 " taskId=%d", 973 task.taskId 974 ) 975 return WindowContainerTransaction().also { wct -> 976 bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) 977 wct.reorder(task.token, true) 978 } 979 } 980 val wct = WindowContainerTransaction() 981 if (useDesktopOverrideDensity()) { 982 wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) 983 } 984 // Desktop Mode is showing and we're launching a new Task - we might need to minimize 985 // a Task. 986 val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) 987 if (taskToMinimize != null) { 988 addPendingMinimizeTransition(transition, taskToMinimize) 989 return wct 990 } 991 return if (wct.isEmpty) null else wct 992 } 993 994 private fun handleFullscreenTaskLaunch( 995 task: RunningTaskInfo, 996 transition: IBinder 997 ): WindowContainerTransaction? { 998 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch") 999 if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { 1000 KtProtoLog.d( 1001 WM_SHELL_DESKTOP_MODE, 1002 "DesktopTasksController: switch fullscreen task to freeform on transition" + 1003 " taskId=%d", 1004 task.taskId 1005 ) 1006 return WindowContainerTransaction().also { wct -> 1007 addMoveToDesktopChanges(wct, task) 1008 // Desktop Mode is already showing and we're launching a new Task - we might need to 1009 // minimize another Task. 1010 val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) 1011 addPendingMinimizeTransition(transition, taskToMinimize) 1012 } 1013 } 1014 return null 1015 } 1016 1017 /** 1018 * If a task is not compatible with desktop mode freeform, it should always be launched in 1019 * fullscreen. 1020 */ 1021 private fun handleIncompatibleTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { 1022 // Already fullscreen, no-op. 1023 if (task.isFullscreen) return null 1024 return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) } 1025 } 1026 1027 /** Handle back navigation by removing wallpaper activity if it's the last active task */ 1028 private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? { 1029 if ( 1030 desktopModeTaskRepository.isOnlyActiveTask(task.taskId) && 1031 desktopModeTaskRepository.wallpaperActivityToken != null 1032 ) { 1033 // Remove wallpaper activity when the last active task is removed 1034 return WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) } 1035 } else { 1036 return null 1037 } 1038 } 1039 1040 private fun addMoveToDesktopChanges( 1041 wct: WindowContainerTransaction, 1042 taskInfo: RunningTaskInfo 1043 ) { 1044 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return 1045 val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! 1046 val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode 1047 val targetWindowingMode = 1048 if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) { 1049 // Display windowing is freeform, set to undefined and inherit it 1050 WINDOWING_MODE_UNDEFINED 1051 } else { 1052 WINDOWING_MODE_FREEFORM 1053 } 1054 if (Flags.enableWindowingDynamicInitialBounds()) { 1055 wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo)) 1056 } 1057 wct.setWindowingMode(taskInfo.token, targetWindowingMode) 1058 wct.reorder(taskInfo.token, true /* onTop */) 1059 if (useDesktopOverrideDensity()) { 1060 wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE) 1061 } 1062 } 1063 1064 private fun addMoveToFullscreenChanges( 1065 wct: WindowContainerTransaction, 1066 taskInfo: RunningTaskInfo 1067 ) { 1068 val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! 1069 val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode 1070 val targetWindowingMode = 1071 if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) { 1072 // Display windowing is fullscreen, set to undefined and inherit it 1073 WINDOWING_MODE_UNDEFINED 1074 } else { 1075 WINDOWING_MODE_FULLSCREEN 1076 } 1077 wct.setWindowingMode(taskInfo.token, targetWindowingMode) 1078 wct.setBounds(taskInfo.token, Rect()) 1079 if (useDesktopOverrideDensity()) { 1080 wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) 1081 } 1082 } 1083 1084 /** 1085 * Adds split screen changes to a transaction. Note that bounds are not reset here due to 1086 * animation; see {@link onDesktopSplitSelectAnimComplete} 1087 */ 1088 private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { 1089 // This windowing mode is to get the transition animation started; once we complete 1090 // split select, we will change windowing mode to undefined and inherit from split stage. 1091 // Going to undefined here causes task to flicker to the top left. 1092 // Cancelling the split select flow will revert it to fullscreen. 1093 wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) 1094 // The task's density may have been overridden in freeform; revert it here as we don't 1095 // want it overridden in multi-window. 1096 wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) 1097 } 1098 1099 /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ 1100 private fun addAndGetMinimizeChangesIfNeeded( 1101 displayId: Int, 1102 wct: WindowContainerTransaction, 1103 newTaskInfo: RunningTaskInfo 1104 ): RunningTaskInfo? { 1105 if (!desktopTasksLimiter.isPresent) return null 1106 return desktopTasksLimiter 1107 .get() 1108 .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskInfo) 1109 } 1110 1111 private fun addPendingMinimizeTransition( 1112 transition: IBinder, 1113 taskToMinimize: RunningTaskInfo? 1114 ) { 1115 if (taskToMinimize == null) return 1116 desktopTasksLimiter.ifPresent { 1117 it.addPendingMinimizeChange(transition, taskToMinimize.displayId, taskToMinimize.taskId) 1118 } 1119 } 1120 1121 /** Enter split by using the focused desktop task in given `displayId`. */ 1122 fun enterSplit(displayId: Int, leftOrTop: Boolean) { 1123 getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) } 1124 } 1125 1126 private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? { 1127 return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> 1128 taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM 1129 } 1130 } 1131 1132 /** 1133 * Requests a task be transitioned from desktop to split select. Applies needed windowing 1134 * changes if this transition is enabled. 1135 */ 1136 @JvmOverloads 1137 fun requestSplit( 1138 taskInfo: RunningTaskInfo, 1139 leftOrTop: Boolean = false 1140 ) { 1141 // If a drag to desktop is in progress, we want to enter split select 1142 // even if the requesting task is already in split. 1143 val isDragging = dragToDesktopTransitionHandler.inProgress 1144 val shouldRequestSplit = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging 1145 if (shouldRequestSplit) { 1146 if (isDragging) { 1147 releaseVisualIndicator() 1148 val cancelState = if (leftOrTop) { 1149 DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT 1150 } else { 1151 DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT 1152 } 1153 dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState) 1154 } else { 1155 val wct = WindowContainerTransaction() 1156 addMoveToSplitChanges(wct, taskInfo) 1157 splitScreenController.requestEnterSplitSelect( 1158 taskInfo, 1159 wct, 1160 if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT, 1161 taskInfo.configuration.windowConfiguration.bounds 1162 ) 1163 } 1164 } 1165 } 1166 1167 private fun getDefaultDensityDpi(): Int { 1168 return context.resources.displayMetrics.densityDpi 1169 } 1170 1171 /** Creates a new instance of the external interface to pass to another process. */ 1172 private fun createExternalInterface(): ExternalInterfaceBinder { 1173 return IDesktopModeImpl(this) 1174 } 1175 1176 /** Get connection interface between sysui and shell */ 1177 fun asDesktopMode(): DesktopMode { 1178 return desktopMode 1179 } 1180 1181 /** 1182 * Perform checks required on drag move. Create/release fullscreen indicator as needed. 1183 * Different sources for x and y coordinates are used due to different needs for each: We want 1184 * split transitions to be based on input coordinates but fullscreen transition to be based on 1185 * task edge coordinate. 1186 * 1187 * @param taskInfo the task being dragged. 1188 * @param taskSurface SurfaceControl of dragged task. 1189 * @param inputX x coordinate of input. Used for checks against left/right edge of screen. 1190 * @param taskBounds bounds of dragged task. Used for checks against status bar height. 1191 */ 1192 fun onDragPositioningMove( 1193 taskInfo: RunningTaskInfo, 1194 taskSurface: SurfaceControl, 1195 inputX: Float, 1196 taskBounds: Rect 1197 ) { 1198 if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return 1199 updateVisualIndicator(taskInfo, taskSurface, inputX, taskBounds.top.toFloat()) 1200 } 1201 1202 fun updateVisualIndicator( 1203 taskInfo: RunningTaskInfo, 1204 taskSurface: SurfaceControl, 1205 inputX: Float, 1206 taskTop: Float 1207 ): DesktopModeVisualIndicator.IndicatorType { 1208 // If the visual indicator does not exist, create it. 1209 val indicator = 1210 visualIndicator 1211 ?: DesktopModeVisualIndicator( 1212 syncQueue, 1213 taskInfo, 1214 displayController, 1215 context, 1216 taskSurface, 1217 rootTaskDisplayAreaOrganizer 1218 ) 1219 if (visualIndicator == null) visualIndicator = indicator 1220 return indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode) 1221 } 1222 1223 /** 1224 * Perform checks required on drag end. If indicator indicates a windowing mode change, perform 1225 * that change. Otherwise, ensure bounds are up to date. 1226 * 1227 * @param taskInfo the task being dragged. 1228 * @param position position of surface when drag ends. 1229 * @param inputCoordinate the coordinates of the motion event 1230 * @param taskBounds the updated bounds of the task being dragged. 1231 */ 1232 fun onDragPositioningEnd( 1233 taskInfo: RunningTaskInfo, 1234 position: Point, 1235 inputCoordinate: PointF, 1236 taskBounds: Rect, 1237 validDragArea: Rect 1238 ) { 1239 if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { 1240 return 1241 } 1242 1243 val indicator = visualIndicator ?: return 1244 val indicatorType = 1245 indicator.updateIndicatorType( 1246 PointF(inputCoordinate.x, taskBounds.top.toFloat()), 1247 taskInfo.windowingMode 1248 ) 1249 when (indicatorType) { 1250 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { 1251 moveToFullscreenWithAnimation( 1252 taskInfo, 1253 position, 1254 DesktopModeTransitionSource.TASK_DRAG 1255 ) 1256 } 1257 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { 1258 releaseVisualIndicator() 1259 snapToHalfScreen(taskInfo, SnapPosition.LEFT) 1260 } 1261 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { 1262 releaseVisualIndicator() 1263 snapToHalfScreen(taskInfo, SnapPosition.RIGHT) 1264 } 1265 DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR -> { 1266 // If task bounds are outside valid drag area, snap them inward and perform a 1267 // transaction to set bounds. 1268 if ( 1269 DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( 1270 taskBounds, 1271 validDragArea 1272 ) 1273 ) { 1274 val wct = WindowContainerTransaction() 1275 wct.setBounds(taskInfo.token, taskBounds) 1276 transitions.startTransition(TRANSIT_CHANGE, wct, null) 1277 } 1278 releaseVisualIndicator() 1279 } 1280 DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { 1281 throw IllegalArgumentException( 1282 "Should not be receiving TO_DESKTOP_INDICATOR for " + "a freeform task." 1283 ) 1284 } 1285 } 1286 // A freeform drag-move ended, remove the indicator immediately. 1287 releaseVisualIndicator() 1288 } 1289 1290 /** 1291 * Perform checks required when drag ends under status bar area. 1292 * 1293 * @param taskInfo the task being dragged. 1294 * @param y height of drag, to be checked against status bar height. 1295 */ 1296 fun onDragPositioningEndThroughStatusBar( 1297 inputCoordinates: PointF, 1298 taskInfo: RunningTaskInfo, 1299 ) { 1300 val indicator = getVisualIndicator() ?: return 1301 val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode) 1302 when (indicatorType) { 1303 DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { 1304 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return 1305 if (Flags.enableWindowingDynamicInitialBounds()) { 1306 finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo)) 1307 } else { 1308 finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout)) 1309 } 1310 } 1311 DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, 1312 DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { 1313 cancelDragToDesktop(taskInfo) 1314 } 1315 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { 1316 requestSplit(taskInfo, leftOrTop = true) 1317 } 1318 DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { 1319 requestSplit(taskInfo, leftOrTop = false) 1320 } 1321 } 1322 } 1323 1324 /** Update the exclusion region for a specified task */ 1325 fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) { 1326 desktopModeTaskRepository.updateTaskExclusionRegions(taskId, exclusionRegion) 1327 } 1328 1329 /** Remove a previously tracked exclusion region for a specified task. */ 1330 fun removeExclusionRegionForTask(taskId: Int) { 1331 desktopModeTaskRepository.removeExclusionRegion(taskId) 1332 } 1333 1334 /** 1335 * Adds a listener to find out about changes in the visibility of freeform tasks. 1336 * 1337 * @param listener the listener to add. 1338 * @param callbackExecutor the executor to call the listener on. 1339 */ 1340 fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) { 1341 desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor) 1342 } 1343 1344 /** 1345 * Adds a listener to track changes to desktop task gesture exclusion regions 1346 * 1347 * @param listener the listener to add. 1348 * @param callbackExecutor the executor to call the listener on. 1349 */ 1350 fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) { 1351 desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor) 1352 } 1353 1354 override fun onUnhandledDrag( 1355 launchIntent: PendingIntent, 1356 dragSurface: SurfaceControl, 1357 onFinishCallback: Consumer<Boolean> 1358 ): Boolean { 1359 // TODO(b/320797628): Pass through which display we are dropping onto 1360 val activeTasks = desktopModeTaskRepository.getActiveTasks(DEFAULT_DISPLAY) 1361 if (!activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { 1362 // Not currently in desktop mode, ignore the drop 1363 return false 1364 } 1365 1366 val launchComponent = getComponent(launchIntent) 1367 if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent)) { 1368 // TODO(b/320797628): Should only return early if there is an existing running task, and 1369 // notify the user as well. But for now, just ignore the drop. 1370 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "Dropped intent does not support multi-instance") 1371 return false 1372 } 1373 1374 // Start a new transition to launch the app 1375 val opts = 1376 ActivityOptions.makeBasic().apply { 1377 launchWindowingMode = WINDOWING_MODE_FREEFORM 1378 pendingIntentLaunchFlags = 1379 Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK 1380 setPendingIntentBackgroundActivityStartMode( 1381 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED 1382 ) 1383 isPendingIntentBackgroundActivityLaunchAllowedByPermission = true 1384 } 1385 val wct = WindowContainerTransaction() 1386 wct.sendPendingIntent(launchIntent, null, opts.toBundle()) 1387 transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) 1388 1389 // Report that this is handled by the listener 1390 onFinishCallback.accept(true) 1391 1392 // We've assumed responsibility of cleaning up the drag surface, so do that now 1393 // TODO(b/320797628): Do an actual animation here for the drag surface 1394 val t = SurfaceControl.Transaction() 1395 t.remove(dragSurface) 1396 t.apply() 1397 return true 1398 } 1399 1400 private fun dump(pw: PrintWriter, prefix: String) { 1401 val innerPrefix = "$prefix " 1402 pw.println("${prefix}DesktopTasksController") 1403 desktopModeTaskRepository.dump(pw, innerPrefix) 1404 } 1405 1406 /** The interface for calls from outside the shell, within the host process. */ 1407 @ExternalThread 1408 private inner class DesktopModeImpl : DesktopMode { 1409 override fun addVisibleTasksListener( 1410 listener: VisibleTasksListener, 1411 callbackExecutor: Executor 1412 ) { 1413 mainExecutor.execute { 1414 this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor) 1415 } 1416 } 1417 1418 override fun addDesktopGestureExclusionRegionListener( 1419 listener: Consumer<Region>, 1420 callbackExecutor: Executor 1421 ) { 1422 mainExecutor.execute { 1423 this@DesktopTasksController.setTaskRegionListener(listener, callbackExecutor) 1424 } 1425 } 1426 1427 override fun moveFocusedTaskToDesktop( 1428 displayId: Int, 1429 transitionSource: DesktopModeTransitionSource 1430 ) { 1431 mainExecutor.execute { 1432 this@DesktopTasksController.moveFocusedTaskToDesktop(displayId, transitionSource) 1433 } 1434 } 1435 1436 override fun moveFocusedTaskToFullscreen( 1437 displayId: Int, 1438 transitionSource: DesktopModeTransitionSource 1439 ) { 1440 mainExecutor.execute { 1441 this@DesktopTasksController.enterFullscreen(displayId, transitionSource) 1442 } 1443 } 1444 1445 override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) { 1446 mainExecutor.execute { this@DesktopTasksController.enterSplit(displayId, leftOrTop) } 1447 } 1448 } 1449 1450 /** The interface for calls from outside the host process. */ 1451 @BinderThread 1452 private class IDesktopModeImpl(private var controller: DesktopTasksController?) : 1453 IDesktopMode.Stub(), ExternalInterfaceBinder { 1454 1455 private lateinit var remoteListener: 1456 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener> 1457 1458 private val listener: VisibleTasksListener = 1459 object : VisibleTasksListener { 1460 override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { 1461 KtProtoLog.v( 1462 WM_SHELL_DESKTOP_MODE, 1463 "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d", 1464 displayId, 1465 visibleTasksCount 1466 ) 1467 remoteListener.call { l -> 1468 l.onTasksVisibilityChanged(displayId, visibleTasksCount) 1469 } 1470 } 1471 } 1472 1473 init { 1474 remoteListener = 1475 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( 1476 controller, 1477 { c -> 1478 c.desktopModeTaskRepository.addVisibleTasksListener( 1479 listener, 1480 c.mainExecutor 1481 ) 1482 }, 1483 { c -> c.desktopModeTaskRepository.removeVisibleTasksListener(listener) } 1484 ) 1485 } 1486 1487 /** Invalidates this instance, preventing future calls from updating the controller. */ 1488 override fun invalidate() { 1489 remoteListener.unregister() 1490 controller = null 1491 } 1492 1493 override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) { 1494 executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c -> 1495 c.showDesktopApps(displayId, remoteTransition) 1496 } 1497 } 1498 1499 override fun showDesktopApp(taskId: Int) { 1500 executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c -> 1501 c.moveTaskToFront(taskId) 1502 } 1503 } 1504 1505 override fun stashDesktopApps(displayId: Int) { 1506 KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: stashDesktopApps is deprecated") 1507 } 1508 1509 override fun hideStashedDesktopApps(displayId: Int) { 1510 KtProtoLog.w( 1511 WM_SHELL_DESKTOP_MODE, 1512 "IDesktopModeImpl: hideStashedDesktopApps is deprecated" 1513 ) 1514 } 1515 1516 override fun getVisibleTaskCount(displayId: Int): Int { 1517 val result = IntArray(1) 1518 executeRemoteCallWithTaskPermission( 1519 controller, 1520 "getVisibleTaskCount", 1521 { controller -> result[0] = controller.getVisibleTaskCount(displayId) }, 1522 true /* blocking */ 1523 ) 1524 return result[0] 1525 } 1526 1527 override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { 1528 executeRemoteCallWithTaskPermission( 1529 controller, 1530 "onDesktopSplitSelectAnimComplete" 1531 ) { c -> 1532 c.onDesktopSplitSelectAnimComplete(taskInfo) 1533 } 1534 } 1535 1536 override fun setTaskListener(listener: IDesktopTaskListener?) { 1537 KtProtoLog.v( 1538 WM_SHELL_DESKTOP_MODE, 1539 "IDesktopModeImpl: set task listener=%s", 1540 listener ?: "null" 1541 ) 1542 executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ -> 1543 listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() 1544 } 1545 } 1546 1547 override fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) { 1548 executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c -> 1549 c.moveToDesktop(taskId, transitionSource = transitionSource) 1550 } 1551 } 1552 } 1553 1554 companion object { 1555 @JvmField 1556 val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = 1557 SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f 1558 } 1559 1560 /** The positions on a screen that a task can snap to. */ 1561 enum class SnapPosition { 1562 RIGHT, 1563 LEFT 1564 } 1565 } 1566