1 /* <lambda>null2 * Copyright (C) 2024 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.ActivityTaskManager.INVALID_TASK_ID 20 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 21 import android.content.Context 22 import android.os.Handler 23 import android.os.IBinder 24 import android.view.SurfaceControl 25 import android.view.WindowManager 26 import android.view.WindowManager.TRANSIT_CLOSE 27 import android.view.WindowManager.TRANSIT_OPEN 28 import android.window.DesktopModeFlags 29 import android.window.TransitionInfo 30 import android.window.TransitionInfo.Change 31 import android.window.TransitionRequestInfo 32 import android.window.WindowContainerTransaction 33 import androidx.annotation.VisibleForTesting 34 import com.android.internal.jank.InteractionJankMonitor 35 import com.android.internal.protolog.ProtoLog 36 import com.android.wm.shell.RootTaskDisplayAreaOrganizer 37 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler 38 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter 39 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 40 import com.android.wm.shell.shared.TransitionUtil 41 import com.android.wm.shell.shared.annotations.ShellMainThread 42 import com.android.wm.shell.sysui.ShellInit 43 import com.android.wm.shell.transition.MixedTransitionHandler 44 import com.android.wm.shell.transition.Transitions 45 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback 46 47 /** The [Transitions.TransitionHandler] coordinates transition handlers in desktop windowing. */ 48 class DesktopMixedTransitionHandler( 49 private val context: Context, 50 private val transitions: Transitions, 51 private val desktopUserRepositories: DesktopUserRepositories, 52 private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler, 53 private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler, 54 private val desktopImmersiveController: DesktopImmersiveController, 55 private val desktopMinimizationTransitionHandler: DesktopMinimizationTransitionHandler, 56 private val interactionJankMonitor: InteractionJankMonitor, 57 @ShellMainThread private val handler: Handler, 58 shellInit: ShellInit, 59 private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, 60 ) : MixedTransitionHandler, FreeformTaskTransitionStarter { 61 62 init { 63 shellInit.addInitCallback({ transitions.addHandler(this) }, this) 64 } 65 66 @VisibleForTesting val pendingMixedTransitions = mutableListOf<PendingMixedTransition>() 67 68 /** Delegates starting transition to [FreeformTaskTransitionHandler]. */ 69 override fun startWindowingModeTransition( 70 targetWindowingMode: Int, 71 wct: WindowContainerTransaction?, 72 ) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct) 73 74 /** 75 * Starts a minimize transition for [taskId], with [isLastTask] which is true if the task going 76 * to be minimized is the last visible task. 77 */ 78 override fun startMinimizedModeTransition( 79 wct: WindowContainerTransaction?, 80 taskId: Int, 81 isLastTask: Boolean, 82 ): IBinder { 83 if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue) { 84 return freeformTaskTransitionHandler.startMinimizedModeTransition( 85 wct, 86 taskId, 87 isLastTask, 88 ) 89 } 90 requireNotNull(wct) 91 return transitions 92 .startTransition(Transitions.TRANSIT_MINIMIZE, wct, /* handler= */ this) 93 .also { transition -> 94 pendingMixedTransitions.add( 95 PendingMixedTransition.Minimize(transition, taskId, isLastTask) 96 ) 97 } 98 } 99 100 /** Delegates starting PiP transition to [FreeformTaskTransitionHandler]. */ 101 override fun startPipTransition(wct: WindowContainerTransaction?): IBinder = 102 freeformTaskTransitionHandler.startPipTransition(wct) 103 104 /** Starts close transition and handles or delegates desktop task close animation. */ 105 override fun startRemoveTransition(wct: WindowContainerTransaction?): IBinder { 106 if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX.isTrue) { 107 return freeformTaskTransitionHandler.startRemoveTransition(wct) 108 } 109 requireNotNull(wct) 110 return transitions 111 .startTransition(WindowManager.TRANSIT_CLOSE, wct, /* handler= */ this) 112 .also { transition -> 113 pendingMixedTransitions.add(PendingMixedTransition.Close(transition)) 114 } 115 } 116 117 /** 118 * Starts a launch transition for [taskId], with an optional [exitingImmersiveTask] if it was 119 * included in the [wct] and is expected to be animated by this handler. 120 */ 121 fun startLaunchTransition( 122 @WindowManager.TransitionType transitionType: Int, 123 wct: WindowContainerTransaction, 124 taskId: Int?, 125 minimizingTaskId: Int? = null, 126 exitingImmersiveTask: Int? = null, 127 ): IBinder { 128 if ( 129 !DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue && 130 !DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue 131 ) { 132 return transitions.startTransition(transitionType, wct, /* handler= */ null) 133 } 134 if (exitingImmersiveTask == null) { 135 logV("Starting mixed launch transition for task#%d", taskId) 136 } else { 137 logV( 138 "Starting mixed launch transition for task#%d with immersive exit of task#%d", 139 taskId, 140 exitingImmersiveTask, 141 ) 142 } 143 return transitions.startTransition(transitionType, wct, /* handler= */ this).also { 144 transition -> 145 pendingMixedTransitions.add( 146 PendingMixedTransition.Launch( 147 transition = transition, 148 launchingTask = taskId, 149 minimizingTask = minimizingTaskId, 150 exitingImmersiveTask = exitingImmersiveTask, 151 ) 152 ) 153 } 154 } 155 156 /** Notifies this handler that there is a pending transition for it to handle. */ 157 fun addPendingMixedTransition(pendingMixedTransition: PendingMixedTransition) { 158 pendingMixedTransitions.add(pendingMixedTransition) 159 } 160 161 /** Returns null, as it only handles transitions started from Shell. */ 162 override fun handleRequest( 163 transition: IBinder, 164 request: TransitionRequestInfo, 165 ): WindowContainerTransaction? = null 166 167 override fun startAnimation( 168 transition: IBinder, 169 info: TransitionInfo, 170 startTransaction: SurfaceControl.Transaction, 171 finishTransaction: SurfaceControl.Transaction, 172 finishCallback: TransitionFinishCallback, 173 ): Boolean { 174 val pending = 175 pendingMixedTransitions.find { pending -> pending.transition == transition } 176 ?: return false.also { logV("No pending desktop transition") } 177 pendingMixedTransitions.remove(pending) 178 logV("Animating pending mixed transition: %s", pending) 179 return when (pending) { 180 is PendingMixedTransition.Close -> 181 animateCloseTransition( 182 transition, 183 info, 184 startTransaction, 185 finishTransaction, 186 finishCallback, 187 ) 188 is PendingMixedTransition.Launch -> 189 animateLaunchTransition( 190 pending, 191 transition, 192 info, 193 startTransaction, 194 finishTransaction, 195 finishCallback, 196 ) 197 is PendingMixedTransition.Minimize -> 198 animateMinimizeTransition( 199 pending, 200 transition, 201 info, 202 startTransaction, 203 finishTransaction, 204 finishCallback, 205 ) 206 } 207 } 208 209 private fun animateCloseTransition( 210 transition: IBinder, 211 info: TransitionInfo, 212 startTransaction: SurfaceControl.Transaction, 213 finishTransaction: SurfaceControl.Transaction, 214 finishCallback: TransitionFinishCallback, 215 ): Boolean { 216 val closeChange = findCloseDesktopTaskChange(info) 217 if (closeChange == null) { 218 logW("Should have closing desktop task") 219 return false 220 } 221 if (isWallpaperActivityClosing(info)) { 222 // If the wallpaper activity is closing then the desktop is closing, animate the closing 223 // desktop by dispatching to other transition handlers. 224 return dispatchCloseLastDesktopTaskAnimation( 225 transition, 226 info, 227 startTransaction, 228 finishTransaction, 229 finishCallback, 230 ) 231 } 232 // Animate close desktop task transition with [CloseDesktopTaskTransitionHandler]. 233 return closeDesktopTaskTransitionHandler.startAnimation( 234 transition, 235 info, 236 startTransaction, 237 finishTransaction, 238 finishCallback, 239 ) 240 } 241 242 private fun animateLaunchTransition( 243 pending: PendingMixedTransition.Launch, 244 transition: IBinder, 245 info: TransitionInfo, 246 startTransaction: SurfaceControl.Transaction, 247 finishTransaction: SurfaceControl.Transaction, 248 finishCallback: TransitionFinishCallback, 249 ): Boolean { 250 // Check if there's also an immersive change during this launch. 251 val immersiveExitChange = 252 pending.exitingImmersiveTask?.let { exitingTask -> findTaskChange(info, exitingTask) } 253 val minimizeChange = 254 pending.minimizingTask?.let { minimizingTask -> findTaskChange(info, minimizingTask) } 255 val launchChange = findDesktopTaskLaunchChange(info, pending.launchingTask) 256 if (launchChange == null) { 257 check(immersiveExitChange == null) 258 logV("No launch Change, returning") 259 return false 260 } 261 262 var subAnimationCount = -1 263 var combinedWct: WindowContainerTransaction? = null 264 val finishCb = TransitionFinishCallback { wct -> 265 --subAnimationCount 266 combinedWct = combinedWct.merge(wct) 267 if (subAnimationCount > 0) return@TransitionFinishCallback 268 finishCallback.onTransitionFinished(combinedWct) 269 } 270 271 logV( 272 "Animating mixed launch transition task#%d, minimizingTask#%s immersiveExitTask#%s", 273 launchChange.taskInfo!!.taskId, 274 minimizeChange?.taskInfo?.taskId, 275 immersiveExitChange?.taskInfo?.taskId, 276 ) 277 if (DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue) { 278 // Only apply minimize change reparenting here if we implement the new app launch 279 // transitions, otherwise this reparenting is handled in the default handler. 280 minimizeChange?.let { 281 applyMinimizeChangeReparenting(info, minimizeChange, startTransaction) 282 } 283 } 284 if (immersiveExitChange != null) { 285 subAnimationCount = 2 286 // Animate the immersive exit change separately. 287 info.changes.remove(immersiveExitChange) 288 desktopImmersiveController.animateResizeChange( 289 immersiveExitChange, 290 startTransaction, 291 finishTransaction, 292 finishCb, 293 ) 294 // Let the leftover/default handler animate the remaining changes. 295 return dispatchToLeftoverHandler( 296 transition, 297 info, 298 startTransaction, 299 finishTransaction, 300 finishCb, 301 ) 302 } 303 // There's nothing to animate separately, so let the left over handler animate 304 // the entire transition. 305 subAnimationCount = 1 306 return dispatchToLeftoverHandler( 307 transition, 308 info, 309 startTransaction, 310 finishTransaction, 311 finishCb, 312 ) 313 } 314 315 private fun animateMinimizeTransition( 316 pending: PendingMixedTransition.Minimize, 317 transition: IBinder, 318 info: TransitionInfo, 319 startTransaction: SurfaceControl.Transaction, 320 finishTransaction: SurfaceControl.Transaction, 321 finishCallback: TransitionFinishCallback, 322 ): Boolean { 323 val shouldAnimate = 324 if (info.type == Transitions.TRANSIT_MINIMIZE) { 325 DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue 326 } else { 327 DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue 328 } 329 if (!shouldAnimate) { 330 return false 331 } 332 333 val minimizeChange = findTaskChange(info, pending.minimizingTask) 334 if (minimizeChange == null) { 335 logW("Should have minimizing desktop task") 336 return false 337 } 338 if (pending.isLastTask) { 339 // Dispatch close desktop task animation to the default transition handlers. 340 return dispatchToLeftoverHandler( 341 transition, 342 info, 343 startTransaction, 344 finishTransaction, 345 finishCallback, 346 ) 347 } 348 349 // Animate minimizing desktop task transition with [DesktopMinimizationTransitionHandler]. 350 return desktopMinimizationTransitionHandler.startAnimation( 351 transition, 352 info, 353 startTransaction, 354 finishTransaction, 355 finishCallback, 356 ) 357 } 358 359 override fun onTransitionConsumed( 360 transition: IBinder, 361 aborted: Boolean, 362 finishTransaction: SurfaceControl.Transaction?, 363 ) { 364 pendingMixedTransitions.removeAll { pending -> pending.transition == transition } 365 super.onTransitionConsumed(transition, aborted, finishTransaction) 366 } 367 368 /** 369 * Dispatch close desktop task animation to the default transition handlers. Allows delegating 370 * it to Launcher to animate in sync with show Home transition. 371 */ 372 private fun dispatchCloseLastDesktopTaskAnimation( 373 transition: IBinder, 374 info: TransitionInfo, 375 startTransaction: SurfaceControl.Transaction, 376 finishTransaction: SurfaceControl.Transaction, 377 finishCallback: TransitionFinishCallback, 378 ): Boolean { 379 // Dispatch the last desktop task closing animation. 380 return dispatchToLeftoverHandler( 381 transition = transition, 382 info = info, 383 startTransaction = startTransaction, 384 finishTransaction = finishTransaction, 385 finishCallback = finishCallback, 386 ) 387 } 388 389 /** 390 * Reparent the minimizing task back to its root display area. 391 * 392 * During the launch/minimize animation the all animated tasks will be reparented to a 393 * transition leash shown in front of other desktop tasks. Reparenting the minimizing task back 394 * to its root display area ensures that task stays behind other desktop tasks during the 395 * animation. 396 */ 397 private fun applyMinimizeChangeReparenting( 398 info: TransitionInfo, 399 minimizeChange: Change, 400 startTransaction: SurfaceControl.Transaction, 401 ) { 402 require(TransitionUtil.isOpeningMode(info.type)) 403 require(minimizeChange.taskInfo != null) 404 val taskInfo = minimizeChange.taskInfo!! 405 require(taskInfo.isFreeform) 406 logV("Reparenting minimizing task#%d", taskInfo.taskId) 407 rootTaskDisplayAreaOrganizer.reparentToDisplayArea( 408 taskInfo.displayId, 409 minimizeChange.leash, 410 startTransaction, 411 ) 412 } 413 414 private fun dispatchToLeftoverHandler( 415 transition: IBinder, 416 info: TransitionInfo, 417 startTransaction: SurfaceControl.Transaction, 418 finishTransaction: SurfaceControl.Transaction, 419 finishCallback: TransitionFinishCallback, 420 doOnFinishCallback: (() -> Unit)? = null, 421 ): Boolean { 422 return transitions.dispatchTransition( 423 transition, 424 info, 425 startTransaction, 426 finishTransaction, 427 { wct -> 428 doOnFinishCallback?.invoke() 429 finishCallback.onTransitionFinished(wct) 430 }, 431 /* skip= */ this, 432 ) != null 433 } 434 435 private fun isWallpaperActivityClosing(info: TransitionInfo) = 436 info.changes.any { change -> 437 TransitionUtil.isClosingMode(change.mode) && 438 change.taskInfo != null && 439 DesktopWallpaperActivity.isWallpaperTask(change.taskInfo!!) 440 } 441 442 private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? { 443 if (info.type != WindowManager.TRANSIT_CLOSE) return null 444 return info.changes.firstOrNull { change -> 445 change.mode == WindowManager.TRANSIT_CLOSE && 446 !change.hasFlags(TransitionInfo.FLAG_IS_WALLPAPER) && 447 change.taskInfo?.taskId != INVALID_TASK_ID && 448 change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM 449 } 450 } 451 452 private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? = 453 info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId } 454 455 private fun findLaunchChange(info: TransitionInfo): TransitionInfo.Change? = 456 info.changes.firstOrNull { change -> 457 change.mode == TRANSIT_OPEN && change.taskInfo != null && change.taskInfo!!.isFreeform 458 } 459 460 private fun findDesktopTaskLaunchChange( 461 info: TransitionInfo, 462 launchTaskId: Int?, 463 ): TransitionInfo.Change? { 464 return if (launchTaskId != null) { 465 // Launching a known task (probably from background or moving to front), so 466 // specifically look for it. 467 val launchChange = findTaskChange(info, launchTaskId) 468 if ( 469 DesktopModeFlags.ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX.isTrue && 470 launchChange == null 471 ) { 472 findLaunchChange(info) 473 } else { 474 launchChange 475 } 476 } else { 477 // Launching a new task, so the first opening freeform task. 478 findLaunchChange(info) 479 } 480 } 481 482 private fun WindowContainerTransaction?.merge( 483 wct: WindowContainerTransaction? 484 ): WindowContainerTransaction? { 485 if (wct == null) return this 486 if (this == null) return wct 487 return this.merge(wct) 488 } 489 490 /** A scheduled transition that will potentially be animated by more than one handler */ 491 sealed class PendingMixedTransition { 492 abstract val transition: IBinder 493 494 /** A task is closing. */ 495 data class Close(override val transition: IBinder) : PendingMixedTransition() 496 497 /** A task is opening or moving to front. */ 498 data class Launch( 499 override val transition: IBinder, 500 val launchingTask: Int?, 501 val minimizingTask: Int?, 502 val exitingImmersiveTask: Int?, 503 ) : PendingMixedTransition() 504 505 /** 506 * A task is minimizing. This should be used for task going to back and some closing cases 507 * with back navigation. 508 */ 509 data class Minimize( 510 override val transition: IBinder, 511 val minimizingTask: Int, 512 val isLastTask: Boolean, 513 ) : PendingMixedTransition() 514 } 515 516 private fun logV(msg: String, vararg arguments: Any?) { 517 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 518 } 519 520 private fun logW(msg: String, vararg arguments: Any?) { 521 ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 522 } 523 524 companion object { 525 private const val TAG = "DesktopMixedTransitionHandler" 526 } 527 } 528