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.annotation.UserIdInt 20 import android.app.ActivityManager 21 import android.app.ActivityManager.RunningTaskInfo 22 import android.app.ActivityOptions 23 import android.app.KeyguardManager 24 import android.app.PendingIntent 25 import android.app.TaskInfo 26 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME 27 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD 28 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 29 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN 30 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW 31 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED 32 import android.app.WindowConfiguration.WindowingMode 33 import android.content.Context 34 import android.content.Intent 35 import android.graphics.Point 36 import android.graphics.PointF 37 import android.graphics.Rect 38 import android.graphics.Region 39 import android.os.Binder 40 import android.os.Bundle 41 import android.os.Handler 42 import android.os.IBinder 43 import android.os.SystemProperties 44 import android.os.UserHandle 45 import android.os.UserManager 46 import android.util.Slog 47 import android.view.Display 48 import android.view.Display.DEFAULT_DISPLAY 49 import android.view.DragEvent 50 import android.view.MotionEvent 51 import android.view.SurfaceControl 52 import android.view.SurfaceControl.Transaction 53 import android.view.WindowManager 54 import android.view.WindowManager.TRANSIT_CHANGE 55 import android.view.WindowManager.TRANSIT_CLOSE 56 import android.view.WindowManager.TRANSIT_NONE 57 import android.view.WindowManager.TRANSIT_OPEN 58 import android.view.WindowManager.TRANSIT_PIP 59 import android.view.WindowManager.TRANSIT_TO_FRONT 60 import android.widget.Toast 61 import android.window.DesktopExperienceFlags 62 import android.window.DesktopModeFlags 63 import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE 64 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER 65 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY 66 import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS 67 import android.window.RemoteTransition 68 import android.window.SplashScreen.SPLASH_SCREEN_STYLE_ICON 69 import android.window.TransitionInfo 70 import android.window.TransitionInfo.Change 71 import android.window.TransitionRequestInfo 72 import android.window.WindowContainerTransaction 73 import androidx.annotation.BinderThread 74 import com.android.internal.annotations.VisibleForTesting 75 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD 76 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE 77 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE 78 import com.android.internal.jank.InteractionJankMonitor 79 import com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx 80 import com.android.internal.protolog.ProtoLog 81 import com.android.internal.util.LatencyTracker 82 import com.android.window.flags.Flags 83 import com.android.wm.shell.Flags.enableFlexibleSplit 84 import com.android.wm.shell.R 85 import com.android.wm.shell.RootTaskDisplayAreaOrganizer 86 import com.android.wm.shell.ShellTaskOrganizer 87 import com.android.wm.shell.bubbles.BubbleController 88 import com.android.wm.shell.common.DisplayController 89 import com.android.wm.shell.common.DisplayLayout 90 import com.android.wm.shell.common.ExternalInterfaceBinder 91 import com.android.wm.shell.common.HomeIntentProvider 92 import com.android.wm.shell.common.MultiInstanceHelper 93 import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent 94 import com.android.wm.shell.common.RemoteCallable 95 import com.android.wm.shell.common.ShellExecutor 96 import com.android.wm.shell.common.SingleInstanceRemoteListener 97 import com.android.wm.shell.common.SyncTransactionQueue 98 import com.android.wm.shell.common.UserProfileContexts 99 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod 100 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason 101 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger 102 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason 103 import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum 104 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState 105 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType 106 import com.android.wm.shell.desktopmode.DesktopRepository.DeskChangeListener 107 import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener 108 import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS 109 import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener 110 import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION 111 import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION 112 import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction 113 import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider 114 import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler 115 import com.android.wm.shell.desktopmode.multidesks.DeskTransition 116 import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer 117 import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver 118 import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener 119 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer 120 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory 121 import com.android.wm.shell.draganddrop.DragAndDropController 122 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter 123 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 124 import com.android.wm.shell.recents.RecentTasksController 125 import com.android.wm.shell.recents.RecentsTransitionHandler 126 import com.android.wm.shell.recents.RecentsTransitionStateListener 127 import com.android.wm.shell.recents.RecentsTransitionStateListener.RecentsTransitionState 128 import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING 129 import com.android.wm.shell.shared.R as SharedR 130 import com.android.wm.shell.shared.TransitionUtil 131 import com.android.wm.shell.shared.annotations.ExternalThread 132 import com.android.wm.shell.shared.annotations.ShellDesktopThread 133 import com.android.wm.shell.shared.annotations.ShellMainThread 134 import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy 135 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus 136 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE 137 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity 138 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource 139 import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason 140 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED 141 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT 142 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT 143 import com.android.wm.shell.splitscreen.SplitScreenController 144 import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE 145 import com.android.wm.shell.sysui.ShellCommandHandler 146 import com.android.wm.shell.sysui.ShellController 147 import com.android.wm.shell.sysui.ShellInit 148 import com.android.wm.shell.sysui.UserChangeListener 149 import com.android.wm.shell.transition.FocusTransitionObserver 150 import com.android.wm.shell.transition.OneShotRemoteHandler 151 import com.android.wm.shell.transition.Transitions 152 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback 153 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility 154 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator 155 import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener 156 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener 157 import com.android.wm.shell.windowdecor.extension.isFullscreen 158 import com.android.wm.shell.windowdecor.extension.isMultiWindow 159 import com.android.wm.shell.windowdecor.extension.requestingImmersive 160 import com.android.wm.shell.windowdecor.tiling.SnapEventHandler 161 import java.io.PrintWriter 162 import java.util.Optional 163 import java.util.concurrent.Executor 164 import java.util.concurrent.TimeUnit 165 import java.util.function.Consumer 166 import kotlin.coroutines.suspendCoroutine 167 import kotlin.jvm.optionals.getOrNull 168 169 /** 170 * A callback to be invoked when a transition is started via |Transitions.startTransition| with the 171 * transition binder token that it produces. 172 * 173 * Useful when multiple components are appending WCT operations to a single transition that is 174 * started outside of their control, and each of them wants to track the transition lifecycle 175 * independently by cross-referencing the transition token with future ready-transitions. 176 */ 177 typealias RunOnTransitStart = (IBinder) -> Unit 178 179 /** Handles moving tasks in and out of desktop */ 180 class DesktopTasksController( 181 private val context: Context, 182 shellInit: ShellInit, 183 private val shellCommandHandler: ShellCommandHandler, 184 private val shellController: ShellController, 185 private val displayController: DisplayController, 186 private val shellTaskOrganizer: ShellTaskOrganizer, 187 private val syncQueue: SyncTransactionQueue, 188 private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, 189 private val dragAndDropController: DragAndDropController, 190 private val transitions: Transitions, 191 private val keyguardManager: KeyguardManager, 192 private val returnToDragStartAnimator: ReturnToDragStartAnimator, 193 private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, 194 private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, 195 private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, 196 private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler, 197 private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, 198 private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, 199 private val desktopImmersiveController: DesktopImmersiveController, 200 private val userRepositories: DesktopUserRepositories, 201 desktopRepositoryInitializer: DesktopRepositoryInitializer, 202 private val recentsTransitionHandler: RecentsTransitionHandler, 203 private val multiInstanceHelper: MultiInstanceHelper, 204 @ShellMainThread private val mainExecutor: ShellExecutor, 205 @ShellDesktopThread private val desktopExecutor: ShellExecutor, 206 private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, 207 private val recentTasksController: RecentTasksController?, 208 private val interactionJankMonitor: InteractionJankMonitor, 209 @ShellMainThread private val handler: Handler, 210 private val focusTransitionObserver: FocusTransitionObserver, 211 private val desktopModeEventLogger: DesktopModeEventLogger, 212 private val desktopModeUiEventLogger: DesktopModeUiEventLogger, 213 private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, 214 private val bubbleController: Optional<BubbleController>, 215 private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, 216 private val desksOrganizer: DesksOrganizer, 217 private val desksTransitionObserver: DesksTransitionObserver, 218 private val userProfileContexts: UserProfileContexts, 219 private val desktopModeCompatPolicy: DesktopModeCompatPolicy, 220 private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, 221 private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler, 222 private val homeIntentProvider: HomeIntentProvider, 223 ) : 224 RemoteCallable<DesktopTasksController>, 225 Transitions.TransitionHandler, 226 DragAndDropController.DragAndDropListener, 227 UserChangeListener { 228 229 private val desktopMode: DesktopModeImpl 230 private var taskRepository: DesktopRepository 231 private var visualIndicator: DesktopModeVisualIndicator? = null 232 private var userId: Int 233 private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = 234 DesktopModeShellCommandHandler(this, focusTransitionObserver) 235 236 private val mOnAnimationFinishedCallback = { releaseVisualIndicator() } 237 private lateinit var snapEventHandler: SnapEventHandler 238 private val dragToDesktopStateListener = 239 object : DragToDesktopStateListener { 240 override fun onCommitToDesktopAnimationStart() { 241 removeVisualIndicator() 242 } 243 244 override fun onCancelToDesktopAnimationEnd() { 245 removeVisualIndicator() 246 } 247 248 override fun onTransitionInterrupted() { 249 removeVisualIndicator() 250 } 251 252 private fun removeVisualIndicator() { 253 visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } 254 } 255 } 256 257 @VisibleForTesting var taskbarDesktopTaskListener: TaskbarDesktopTaskListener? = null 258 259 @VisibleForTesting 260 var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener? = null 261 262 /** Task id of the task currently being dragged from fullscreen/split. */ 263 val draggingTaskId 264 get() = dragToDesktopTransitionHandler.draggingTaskId 265 266 @RecentsTransitionState private var recentsTransitionState = TRANSITION_STATE_NOT_RUNNING 267 268 private lateinit var splitScreenController: SplitScreenController 269 lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter 270 // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun. 271 // Used to prevent handleRequest from moving the new fullscreen task to freeform. 272 private var dragAndDropFullscreenCookie: Binder? = null 273 274 // A listener that is invoked after a desk has been remove from the system. */ 275 var onDeskRemovedListener: OnDeskRemovedListener? = null 276 277 init { 278 desktopMode = DesktopModeImpl() 279 if (DesktopModeStatus.canEnterDesktopMode(context)) { 280 shellInit.addInitCallback({ onInit() }, this) 281 } 282 userId = ActivityManager.getCurrentUser() 283 taskRepository = userRepositories.getProfile(userId) 284 285 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 286 desktopRepositoryInitializer.deskRecreationFactory = 287 DeskRecreationFactory { deskUserId, destinationDisplayId, _ -> 288 createDeskSuspending(displayId = destinationDisplayId, userId = deskUserId) 289 } 290 } 291 } 292 293 private fun onInit() { 294 logD("onInit") 295 shellCommandHandler.addDumpCallback(this::dump, this) 296 shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, this) 297 shellController.addExternalInterface( 298 IDesktopMode.DESCRIPTOR, 299 { createExternalInterface() }, 300 this, 301 ) 302 shellController.addUserChangeListener(this) 303 // Update the current user id again because it might be updated between init and onInit(). 304 updateCurrentUser(ActivityManager.getCurrentUser()) 305 transitions.addHandler(this) 306 dragToDesktopTransitionHandler.dragToDesktopStateListener = dragToDesktopStateListener 307 recentsTransitionHandler.addTransitionStateListener( 308 object : RecentsTransitionStateListener { 309 override fun onTransitionStateChanged(@RecentsTransitionState state: Int) { 310 logV( 311 "Recents transition state changed: %s", 312 RecentsTransitionStateListener.stateToString(state), 313 ) 314 recentsTransitionState = state 315 snapEventHandler.onOverviewAnimationStateChange( 316 RecentsTransitionStateListener.isAnimating(state) 317 ) 318 } 319 } 320 ) 321 dragAndDropController.addListener(this) 322 } 323 324 @VisibleForTesting 325 fun getVisualIndicator(): DesktopModeVisualIndicator? { 326 return visualIndicator 327 } 328 329 fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { 330 toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) 331 enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) 332 dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener 333 desktopImmersiveController.onTaskResizeAnimationListener = listener 334 } 335 336 fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { 337 returnToDragStartAnimator.setTaskRepositionAnimationListener(listener) 338 } 339 340 /** Setter needed to avoid cyclic dependency. */ 341 fun setSplitScreenController(controller: SplitScreenController) { 342 splitScreenController = controller 343 dragToDesktopTransitionHandler.setSplitScreenController(controller) 344 } 345 346 /** Setter to handle snap events */ 347 fun setSnapEventHandler(handler: SnapEventHandler) { 348 snapEventHandler = handler 349 } 350 351 /** Returns the transition type for the given remote transition. */ 352 private fun transitionType(remoteTransition: RemoteTransition?): Int { 353 if (remoteTransition == null) { 354 logV("RemoteTransition is null") 355 return TRANSIT_NONE 356 } 357 return TRANSIT_TO_FRONT 358 } 359 360 /** Show all tasks, that are part of the desktop, on top of launcher */ 361 @Deprecated("Use activateDesk() instead.", ReplaceWith("activateDesk()")) 362 fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) { 363 logV("showDesktopApps") 364 activateDefaultDeskInDisplay(displayId, remoteTransition) 365 } 366 367 /** Returns whether the given display has an active desk. */ 368 fun isAnyDeskActive(displayId: Int): Boolean = taskRepository.isAnyDeskActive(displayId) 369 370 /** 371 * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on 372 * top in Desktop Mode. 373 * 374 * TODO: b/362720497 - consolidate with [isAnyDeskActive]. 375 * - top-transparent-fullscreen case: should not be needed if we allow it to launch inside 376 * the desk in fullscreen instead of force-exiting desktop and having to trick this method 377 * into thinking it is in desktop mode when a task in this state exists. 378 */ 379 fun isDesktopModeShowing(displayId: Int): Boolean { 380 val hasVisibleTasks = taskRepository.isAnyDeskActive(displayId) 381 val hasTopTransparentFullscreenTask = 382 taskRepository.getTopTransparentFullscreenTaskId(displayId) != null 383 if ( 384 DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC 385 .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() 386 ) { 387 logV( 388 "isDesktopModeShowing: hasVisibleTasks=%s hasTopTransparentFullscreenTask=%s", 389 hasVisibleTasks, 390 hasTopTransparentFullscreenTask, 391 ) 392 return hasVisibleTasks || hasTopTransparentFullscreenTask 393 } 394 logV("isDesktopModeShowing: hasVisibleTasks=%s", hasVisibleTasks) 395 return hasVisibleTasks 396 } 397 398 /** Moves focused task to desktop mode for given [displayId]. */ 399 fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) { 400 val allFocusedTasks = getAllFocusedTasks(displayId) 401 when (allFocusedTasks.size) { 402 0 -> return 403 // Full screen case 404 1 -> 405 moveTaskToDefaultDeskAndActivate( 406 allFocusedTasks.single().taskId, 407 transitionSource = transitionSource, 408 ) 409 // Split-screen case where there are two focused tasks, then we find the child 410 // task to move to desktop. 411 2 -> 412 moveTaskToDefaultDeskAndActivate( 413 getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]).taskId, 414 transitionSource = transitionSource, 415 ) 416 else -> 417 logW( 418 "DesktopTasksController: Cannot enter desktop, expected less " + 419 "than 3 focused tasks but found %d", 420 allFocusedTasks.size, 421 ) 422 } 423 } 424 425 /** 426 * Returns all focused tasks in full screen or split screen mode in [displayId] when it is not 427 * the home activity. 428 */ 429 private fun getAllFocusedTasks(displayId: Int): List<RunningTaskInfo> = 430 shellTaskOrganizer.getRunningTasks(displayId).filter { 431 it.isFocused && 432 (it.windowingMode == WINDOWING_MODE_FULLSCREEN || 433 it.windowingMode == WINDOWING_MODE_MULTI_WINDOW) && 434 it.activityType != ACTIVITY_TYPE_HOME 435 } 436 437 /** Returns child task from two focused tasks in split screen mode. */ 438 private fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) = 439 if (task1.taskId == task2.parentTaskId) task2 else task1 440 441 private fun forceEnterDesktop(displayId: Int): Boolean { 442 if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)) { 443 return false 444 } 445 446 // Secondary displays are always desktop-first 447 if (displayId != DEFAULT_DISPLAY) { 448 return true 449 } 450 451 val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) 452 // A non-organized display (e.g., non-trusted virtual displays used in CTS) doesn't have 453 // TDA. 454 if (tdaInfo == null) { 455 logW( 456 "forceEnterDesktop cannot find DisplayAreaInfo for displayId=%d. This could happen" + 457 " when the display is a non-trusted virtual display.", 458 displayId, 459 ) 460 return false 461 } 462 val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode 463 val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM 464 return isFreeformDisplay 465 } 466 467 /** Called when the recents transition that started while in desktop is finishing. */ 468 fun onRecentsInDesktopAnimationFinishing( 469 transition: IBinder, 470 finishWct: WindowContainerTransaction, 471 returnToApp: Boolean, 472 ) { 473 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return 474 logV("onRecentsInDesktopAnimationFinishing returnToApp=%b", returnToApp) 475 if (returnToApp) return 476 // Home/Recents only exists in the default display. 477 val activeDesk = taskRepository.getActiveDeskId(DEFAULT_DISPLAY) ?: return 478 // Not going back to the active desk, deactivate it. 479 val runOnTransitStart = 480 performDesktopExitCleanUp( 481 wct = finishWct, 482 deskId = activeDesk, 483 displayId = DEFAULT_DISPLAY, 484 willExitDesktop = true, 485 shouldEndUpAtHome = true, 486 fromRecentsTransition = true, 487 ) 488 runOnTransitStart?.invoke(transition) 489 } 490 491 /** Adds a new desk to the given display for the given user. */ 492 fun createDesk(displayId: Int, userId: Int = this.userId) { 493 logV("addDesk displayId=%d, userId=%d", displayId, userId) 494 val repository = userRepositories.getProfile(userId) 495 createDesk(displayId, userId) { deskId -> 496 if (deskId == null) { 497 logW("Failed to add desk in displayId=%d for userId=%d", displayId, userId) 498 } else { 499 repository.addDesk(displayId = displayId, deskId = deskId) 500 } 501 } 502 } 503 504 private fun createDesk(displayId: Int, userId: Int = this.userId, onResult: (Int?) -> Unit) { 505 if (displayId == Display.INVALID_DISPLAY) { 506 logW("createDesk attempt with invalid displayId", displayId) 507 onResult(null) 508 return 509 } 510 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 511 // In single-desk, the desk reuses the display id. 512 logD("createDesk reusing displayId=%d for single-desk", displayId) 513 onResult(displayId) 514 return 515 } 516 if ( 517 DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue && 518 UserManager.isHeadlessSystemUserMode() && 519 UserHandle.USER_SYSTEM == userId 520 ) { 521 logW("createDesk ignoring attempt for system user") 522 return 523 } 524 desksOrganizer.createDesk(displayId, userId) { deskId -> 525 logD( 526 "createDesk obtained deskId=%d for displayId=%d and userId=%d", 527 deskId, 528 displayId, 529 userId, 530 ) 531 onResult(deskId) 532 } 533 } 534 535 private suspend fun createDeskSuspending(displayId: Int, userId: Int = this.userId): Int? = 536 suspendCoroutine { cont -> 537 createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) } 538 } 539 540 /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ 541 @JvmOverloads 542 fun moveTaskToDefaultDeskAndActivate( 543 taskId: Int, 544 wct: WindowContainerTransaction = WindowContainerTransaction(), 545 transitionSource: DesktopModeTransitionSource, 546 remoteTransition: RemoteTransition? = null, 547 callback: IMoveToDesktopCallback? = null, 548 ): Boolean { 549 val task = 550 shellTaskOrganizer.getRunningTaskInfo(taskId) 551 ?: recentTasksController?.findTaskInBackground(taskId) 552 if (task == null) { 553 logW("moveTaskToDefaultDeskAndActivate taskId=%d not found", taskId) 554 return false 555 } 556 val deskId = getDefaultDeskId(task.displayId) 557 return moveTaskToDesk( 558 taskId = taskId, 559 deskId = deskId, 560 wct = wct, 561 transitionSource = transitionSource, 562 remoteTransition = remoteTransition, 563 ) 564 } 565 566 /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ 567 fun moveTaskToDesk( 568 taskId: Int, 569 deskId: Int, 570 wct: WindowContainerTransaction = WindowContainerTransaction(), 571 transitionSource: DesktopModeTransitionSource, 572 remoteTransition: RemoteTransition? = null, 573 callback: IMoveToDesktopCallback? = null, 574 ): Boolean { 575 val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) 576 if (runningTask != null) { 577 return moveRunningTaskToDesk( 578 task = runningTask, 579 deskId = deskId, 580 wct = wct, 581 transitionSource = transitionSource, 582 remoteTransition = remoteTransition, 583 callback = callback, 584 ) 585 } 586 val backgroundTask = recentTasksController?.findTaskInBackground(taskId) 587 if (backgroundTask != null) { 588 // TODO: b/391484662 - add support for |deskId|. 589 return moveBackgroundTaskToDesktop( 590 taskId, 591 wct, 592 transitionSource, 593 remoteTransition, 594 callback, 595 ) 596 } 597 logW("moveTaskToDesk taskId=%d not found", taskId) 598 return false 599 } 600 601 private fun moveBackgroundTaskToDesktop( 602 taskId: Int, 603 wct: WindowContainerTransaction, 604 transitionSource: DesktopModeTransitionSource, 605 remoteTransition: RemoteTransition? = null, 606 callback: IMoveToDesktopCallback? = null, 607 ): Boolean { 608 val task = recentTasksController?.findTaskInBackground(taskId) 609 if (task == null) { 610 logW("moveBackgroundTaskToDesktop taskId=%d not found", taskId) 611 return false 612 } 613 logV("moveBackgroundTaskToDesktop with taskId=%d", taskId) 614 val deskId = getDefaultDeskId(task.displayId) 615 val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) 616 val exitResult = 617 desktopImmersiveController.exitImmersiveIfApplicable( 618 wct = wct, 619 displayId = DEFAULT_DISPLAY, 620 excludeTaskId = taskId, 621 reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, 622 ) 623 wct.startTask( 624 taskId, 625 ActivityOptions.makeBasic() 626 .apply { launchWindowingMode = WINDOWING_MODE_FREEFORM } 627 .toBundle(), 628 ) 629 630 val transition: IBinder 631 if (remoteTransition != null) { 632 val transitionType = transitionType(remoteTransition) 633 val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) 634 transition = transitions.startTransition(transitionType, wct, remoteTransitionHandler) 635 remoteTransitionHandler.setTransition(transition) 636 } else { 637 // TODO(343149901): Add DPI changes for task launch 638 transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) 639 invokeCallbackToOverview(transition, callback) 640 } 641 desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( 642 FREEFORM_ANIMATION_DURATION 643 ) 644 runOnTransitStart?.invoke(transition) 645 exitResult.asExit()?.runOnTransitionStart?.invoke(transition) 646 return true 647 } 648 649 /** Moves a running task to desktop. */ 650 private fun moveRunningTaskToDesk( 651 task: RunningTaskInfo, 652 deskId: Int, 653 wct: WindowContainerTransaction = WindowContainerTransaction(), 654 transitionSource: DesktopModeTransitionSource, 655 remoteTransition: RemoteTransition? = null, 656 callback: IMoveToDesktopCallback? = null, 657 ): Boolean { 658 if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) { 659 logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId) 660 return false 661 } 662 val displayId = taskRepository.getDisplayForDesk(deskId) 663 logV( 664 "moveRunningTaskToDesk taskId=%d deskId=%d displayId=%d", 665 task.taskId, 666 deskId, 667 displayId, 668 ) 669 exitSplitIfApplicable(wct, task) 670 val exitResult = 671 desktopImmersiveController.exitImmersiveIfApplicable( 672 wct = wct, 673 displayId = displayId, 674 excludeTaskId = task.taskId, 675 reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, 676 ) 677 678 val runOnTransitStart = addDeskActivationWithMovingTaskChanges(deskId, wct, task) 679 680 val transition: IBinder 681 if (remoteTransition != null) { 682 val transitionType = transitionType(remoteTransition) 683 val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) 684 transition = transitions.startTransition(transitionType, wct, remoteTransitionHandler) 685 remoteTransitionHandler.setTransition(transition) 686 } else { 687 transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) 688 invokeCallbackToOverview(transition, callback) 689 } 690 desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( 691 FREEFORM_ANIMATION_DURATION 692 ) 693 runOnTransitStart?.invoke(transition) 694 exitResult.asExit()?.runOnTransitionStart?.invoke(transition) 695 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 696 taskRepository.setActiveDesk(displayId = displayId, deskId = deskId) 697 } 698 return true 699 } 700 701 private fun invokeCallbackToOverview(transition: IBinder, callback: IMoveToDesktopCallback?) { 702 // TODO: b/333524374 - Remove this later. 703 // This is a temporary implementation for adding CUJ end and 704 // should be removed when animation is moved to launcher through remote transition. 705 if (callback != null) { 706 overviewToDesktopTransitionObserver.addPendingOverviewTransition(transition, callback) 707 } 708 } 709 710 /** 711 * The first part of the animated drag to desktop transition. This is followed with a call to 712 * [finalizeDragToDesktop] or [cancelDragToDesktop]. 713 */ 714 fun startDragToDesktop( 715 taskInfo: RunningTaskInfo, 716 dragToDesktopValueAnimator: MoveToDesktopAnimator, 717 taskSurface: SurfaceControl, 718 dragInterruptedCallback: Runnable, 719 ) { 720 logV("startDragToDesktop taskId=%d", taskInfo.taskId) 721 val jankConfigBuilder = 722 InteractionJankMonitor.Configuration.Builder.withSurface( 723 CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD, 724 context, 725 taskSurface, 726 handler, 727 ) 728 .setTimeout(APP_HANDLE_DRAG_HOLD_CUJ_TIMEOUT_MS) 729 interactionJankMonitor.begin(jankConfigBuilder) 730 dragToDesktopTransitionHandler.startDragToDesktopTransition( 731 taskInfo, 732 dragToDesktopValueAnimator, 733 visualIndicator, 734 dragInterruptedCallback, 735 ) 736 } 737 738 /** 739 * The second part of the animated drag to desktop transition, called after 740 * [startDragToDesktop]. 741 */ 742 private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) { 743 val deskId = getDefaultDeskId(taskInfo.displayId) 744 ProtoLog.v( 745 WM_SHELL_DESKTOP_MODE, 746 "DesktopTasksController: finalizeDragToDesktop taskId=%d deskId=%d", 747 taskInfo.taskId, 748 deskId, 749 ) 750 val wct = WindowContainerTransaction() 751 exitSplitIfApplicable(wct, taskInfo) 752 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 753 // |moveHomeTask| is also called in |bringDesktopAppsToFrontBeforeShowingNewTask|, so 754 // this shouldn't be necessary at all. 755 if (Flags.enablePerDisplayDesktopWallpaperActivity()) { 756 moveHomeTask(taskInfo.displayId, wct) 757 } else { 758 moveHomeTask(context.displayId, wct) 759 } 760 } 761 val runOnTransitStart = addDeskActivationWithMovingTaskChanges(deskId, wct, taskInfo) 762 val exitResult = 763 desktopImmersiveController.exitImmersiveIfApplicable( 764 wct = wct, 765 displayId = taskInfo.displayId, 766 excludeTaskId = null, 767 reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, 768 ) 769 val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) 770 desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( 771 DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS.toInt() 772 ) 773 if (transition != null) { 774 runOnTransitStart?.invoke(transition) 775 exitResult.asExit()?.runOnTransitionStart?.invoke(transition) 776 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 777 taskRepository.setActiveDesk(displayId = taskInfo.displayId, deskId = deskId) 778 } 779 } else { 780 LatencyTracker.getInstance(context) 781 .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) 782 } 783 } 784 785 /** 786 * Perform needed cleanup transaction once animation is complete. Bounds need to be set here 787 * instead of initial wct to both avoid flicker and to have task bounds to use for the staging 788 * animation. 789 * 790 * @param taskInfo task entering split that requires a bounds update 791 */ 792 fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { 793 val wct = WindowContainerTransaction() 794 wct.setBounds(taskInfo.token, Rect()) 795 wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED) 796 shellTaskOrganizer.applyTransaction(wct) 797 } 798 799 /** 800 * Perform clean up of the desktop wallpaper activity if the closed window task is the last 801 * active task. 802 * 803 * @param wct transaction to modify if the last active task is closed 804 * @param displayId display id of the window that's being closed 805 * @param taskId task id of the window that's being closed 806 */ 807 fun onDesktopWindowClose( 808 wct: WindowContainerTransaction, 809 displayId: Int, 810 taskInfo: RunningTaskInfo, 811 ): ((IBinder) -> Unit) { 812 val taskId = taskInfo.taskId 813 val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) 814 if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 815 error("Did not find desk for task: $taskId") 816 } 817 snapEventHandler.removeTaskIfTiled(displayId, taskId) 818 val shouldExitDesktop = 819 willExitDesktop( 820 triggerTaskId = taskInfo.taskId, 821 displayId = displayId, 822 forceExitDesktop = false, 823 ) 824 val desktopExitRunnable = 825 performDesktopExitCleanUp( 826 wct = wct, 827 deskId = deskId, 828 displayId = displayId, 829 willExitDesktop = shouldExitDesktop, 830 shouldEndUpAtHome = true, 831 ) 832 833 taskRepository.addClosingTask(displayId = displayId, deskId = deskId, taskId = taskId) 834 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( 835 doesAnyTaskRequireTaskbarRounding(displayId, taskId) 836 ) 837 838 val immersiveRunnable = 839 desktopImmersiveController 840 .exitImmersiveIfApplicable( 841 wct = wct, 842 taskInfo = taskInfo, 843 reason = DesktopImmersiveController.ExitReason.CLOSED, 844 ) 845 .asExit() 846 ?.runOnTransitionStart 847 return { transitionToken -> 848 immersiveRunnable?.invoke(transitionToken) 849 desktopExitRunnable?.invoke(transitionToken) 850 } 851 } 852 853 fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { 854 val wct = WindowContainerTransaction() 855 val taskId = taskInfo.taskId 856 val displayId = taskInfo.displayId 857 val deskId = 858 taskRepository.getDeskIdForTask(taskInfo.taskId) 859 ?: if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 860 logW("minimizeTask: desk not found for task: ${taskInfo.taskId}") 861 return 862 } else { 863 getDefaultDeskId(taskInfo.displayId) 864 } 865 val isLastTask = 866 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 867 taskRepository.isOnlyVisibleNonClosingTaskInDesk( 868 taskId = taskId, 869 deskId = checkNotNull(deskId) { "Expected non-null deskId" }, 870 displayId = displayId, 871 ) 872 } else { 873 taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId) 874 } 875 val isMinimizingToPip = 876 DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue && 877 (taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false) 878 879 // If task is going to PiP, start a PiP transition instead of a minimize transition 880 if (isMinimizingToPip) { 881 val requestInfo = 882 TransitionRequestInfo( 883 TRANSIT_PIP, 884 /* triggerTask= */ null, 885 taskInfo, 886 /* remoteTransition= */ null, 887 /* displayChange= */ null, 888 /* flags= */ 0, 889 ) 890 val requestRes = 891 transitions.dispatchRequest(SYNTHETIC_TRANSITION, requestInfo, /* skip= */ null) 892 wct.merge(requestRes.second, true) 893 894 // If the task minimizing to PiP is the last task, modify wct to perform Desktop cleanup 895 var desktopExitRunnable: RunOnTransitStart? = null 896 if (isLastTask) { 897 desktopExitRunnable = 898 performDesktopExitCleanUp( 899 wct = wct, 900 deskId = deskId, 901 displayId = displayId, 902 willExitDesktop = true, 903 ) 904 } 905 val transition = freeformTaskTransitionStarter.startPipTransition(wct) 906 desktopExitRunnable?.invoke(transition) 907 } else { 908 snapEventHandler.removeTaskIfTiled(displayId, taskId) 909 val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false) 910 val desktopExitRunnable = 911 performDesktopExitCleanUp( 912 wct = wct, 913 deskId = deskId, 914 displayId = displayId, 915 willExitDesktop = willExitDesktop, 916 ) 917 // Notify immersive handler as it might need to exit immersive state. 918 val exitResult = 919 desktopImmersiveController.exitImmersiveIfApplicable( 920 wct = wct, 921 taskInfo = taskInfo, 922 reason = DesktopImmersiveController.ExitReason.MINIMIZED, 923 ) 924 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 925 desksOrganizer.minimizeTask( 926 wct = wct, 927 deskId = checkNotNull(deskId) { "Expected non-null deskId" }, 928 task = taskInfo, 929 ) 930 } else { 931 wct.reorder(taskInfo.token, /* onTop= */ false) 932 } 933 val transition = 934 freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask) 935 desktopTasksLimiter.ifPresent { 936 it.addPendingMinimizeChange( 937 transition = transition, 938 displayId = displayId, 939 taskId = taskId, 940 minimizeReason = minimizeReason, 941 ) 942 } 943 exitResult.asExit()?.runOnTransitionStart?.invoke(transition) 944 desktopExitRunnable?.invoke(transition) 945 } 946 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( 947 doesAnyTaskRequireTaskbarRounding(displayId, taskId) 948 ) 949 } 950 951 /** Move a task with given `taskId` to fullscreen */ 952 fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) { 953 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> 954 snapEventHandler.removeTaskIfTiled(task.displayId, taskId) 955 moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource) 956 } 957 } 958 959 /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */ 960 fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) { 961 getFocusedFreeformTask(displayId)?.let { 962 snapEventHandler.removeTaskIfTiled(displayId, it.taskId) 963 moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource) 964 } 965 } 966 967 private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { 968 if (splitScreenController.isTaskInSplitScreen(taskInfo.taskId)) { 969 splitScreenController.prepareExitSplitScreen( 970 wct, 971 splitScreenController.getStageOfTask(taskInfo.taskId), 972 EXIT_REASON_DESKTOP_MODE, 973 ) 974 splitScreenController.transitionHandler?.onSplitToDesktop() 975 } 976 } 977 978 /** 979 * The second part of the animated drag to desktop transition, called after 980 * [startDragToDesktop]. 981 */ 982 fun cancelDragToDesktop(task: RunningTaskInfo) { 983 logV("cancelDragToDesktop taskId=%d", task.taskId) 984 dragToDesktopTransitionHandler.cancelDragToDesktopTransition( 985 DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL 986 ) 987 } 988 989 private fun moveToFullscreenWithAnimation( 990 task: RunningTaskInfo, 991 position: Point, 992 transitionSource: DesktopModeTransitionSource, 993 ) { 994 logV("moveToFullscreenWithAnimation taskId=%d", task.taskId) 995 val wct = WindowContainerTransaction() 996 val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceExitDesktop = true) 997 val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop) 998 999 // We are moving a freeform task to fullscreen, put the home task under the fullscreen task. 1000 if (!forceEnterDesktop(task.displayId)) { 1001 moveHomeTask(task.displayId, wct) 1002 wct.reorder(task.token, /* onTop= */ true) 1003 } 1004 1005 val transition = 1006 exitDesktopTaskTransitionHandler.startTransition( 1007 transitionSource, 1008 wct, 1009 position, 1010 mOnAnimationFinishedCallback, 1011 ) 1012 deactivationRunnable?.invoke(transition) 1013 1014 // handles case where we are moving to full screen without closing all DW tasks. 1015 if ( 1016 !taskRepository.isOnlyVisibleNonClosingTask(task.taskId) 1017 // This callback is already invoked by |addMoveToFullscreenChanges| when this flag is 1018 // enabled. 1019 && !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue 1020 ) { 1021 desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( 1022 FULLSCREEN_ANIMATION_DURATION 1023 ) 1024 } 1025 } 1026 1027 /** 1028 * Move a task to the front, using [remoteTransition]. 1029 * 1030 * Note: beyond moving a task to the front, this method will minimize a task if we reach the 1031 * Desktop task limit, so [remoteTransition] should also handle any such minimize change. 1032 */ 1033 @JvmOverloads 1034 fun moveTaskToFront( 1035 taskId: Int, 1036 remoteTransition: RemoteTransition? = null, 1037 unminimizeReason: UnminimizeReason, 1038 ) { 1039 val task = shellTaskOrganizer.getRunningTaskInfo(taskId) 1040 if (task == null) { 1041 moveBackgroundTaskToFront(taskId, remoteTransition, unminimizeReason) 1042 } else { 1043 moveTaskToFront(task, remoteTransition, unminimizeReason) 1044 } 1045 } 1046 1047 /** 1048 * Launch a background task in desktop. Note that this should be used when we are already in 1049 * desktop. If outside of desktop and want to launch a background task in desktop, use 1050 * [moveBackgroundTaskToDesktop] instead. 1051 */ 1052 private fun moveBackgroundTaskToFront( 1053 taskId: Int, 1054 remoteTransition: RemoteTransition?, 1055 unminimizeReason: UnminimizeReason, 1056 ) { 1057 logV("moveBackgroundTaskToFront taskId=%s", taskId) 1058 val wct = WindowContainerTransaction() 1059 wct.startTask( 1060 taskId, 1061 ActivityOptions.makeBasic() 1062 .apply { launchWindowingMode = WINDOWING_MODE_FREEFORM } 1063 .toBundle(), 1064 ) 1065 val deskId = taskRepository.getDeskIdForTask(taskId) ?: getDefaultDeskId(DEFAULT_DISPLAY) 1066 startLaunchTransition( 1067 TRANSIT_OPEN, 1068 wct, 1069 taskId, 1070 deskId = deskId, 1071 displayId = DEFAULT_DISPLAY, 1072 remoteTransition = remoteTransition, 1073 unminimizeReason = unminimizeReason, 1074 ) 1075 } 1076 1077 /** 1078 * Move a task to the front, using [remoteTransition]. 1079 * 1080 * Note: beyond moving a task to the front, this method will minimize a task if we reach the 1081 * Desktop task limit, so [remoteTransition] should also handle any such minimize change. 1082 */ 1083 @JvmOverloads 1084 fun moveTaskToFront( 1085 taskInfo: RunningTaskInfo, 1086 remoteTransition: RemoteTransition? = null, 1087 unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN, 1088 ) { 1089 val deskId = 1090 taskRepository.getDeskIdForTask(taskInfo.taskId) ?: getDefaultDeskId(taskInfo.displayId) 1091 logV("moveTaskToFront taskId=%s deskId=%s", taskInfo.taskId, deskId) 1092 // If a task is tiled, another task should be brought to foreground with it so let 1093 // tiling controller handle the request. 1094 if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) { 1095 return 1096 } 1097 val wct = WindowContainerTransaction() 1098 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 1099 desksOrganizer.reorderTaskToFront(wct, deskId, taskInfo) 1100 } else { 1101 wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true) 1102 } 1103 startLaunchTransition( 1104 transitionType = TRANSIT_TO_FRONT, 1105 wct = wct, 1106 launchingTaskId = taskInfo.taskId, 1107 remoteTransition = remoteTransition, 1108 deskId = deskId, 1109 displayId = taskInfo.displayId, 1110 unminimizeReason = unminimizeReason, 1111 ) 1112 } 1113 1114 @VisibleForTesting 1115 fun startLaunchTransition( 1116 transitionType: Int, 1117 wct: WindowContainerTransaction, 1118 launchingTaskId: Int?, 1119 remoteTransition: RemoteTransition? = null, 1120 deskId: Int, 1121 displayId: Int, 1122 unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN, 1123 ): IBinder { 1124 logV( 1125 "startLaunchTransition type=%s launchingTaskId=%d deskId=%d displayId=%d", 1126 WindowManager.transitTypeToString(transitionType), 1127 launchingTaskId, 1128 deskId, 1129 displayId, 1130 ) 1131 // TODO: b/397619806 - Consolidate sharable logic with [handleFreeformTaskLaunch]. 1132 var launchTransaction = wct 1133 val taskIdToMinimize = 1134 addAndGetMinimizeChanges( 1135 deskId, 1136 launchTransaction, 1137 newTaskId = launchingTaskId, 1138 launchingNewIntent = launchingTaskId == null, 1139 ) 1140 val exitImmersiveResult = 1141 desktopImmersiveController.exitImmersiveIfApplicable( 1142 wct = launchTransaction, 1143 displayId = displayId, 1144 excludeTaskId = launchingTaskId, 1145 reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, 1146 ) 1147 var activationRunOnTransitStart: RunOnTransitStart? = null 1148 val shouldActivateDesk = 1149 when { 1150 DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue -> 1151 !taskRepository.isDeskActive(deskId) 1152 DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue -> { 1153 !isDesktopModeShowing(displayId) 1154 } 1155 else -> false 1156 } 1157 if (shouldActivateDesk) { 1158 val activateDeskWct = WindowContainerTransaction() 1159 // TODO: b/391485148 - pass in the launching task here to apply task-limit policy, 1160 // but make sure to not do it twice since it is also done at the start of this 1161 // function. 1162 activationRunOnTransitStart = addDeskActivationChanges(deskId, activateDeskWct) 1163 // Desk activation must be handled before app launch-related transactions. 1164 activateDeskWct.merge(launchTransaction, /* transfer= */ true) 1165 launchTransaction = activateDeskWct 1166 desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( 1167 FREEFORM_ANIMATION_DURATION 1168 ) 1169 } 1170 val t = 1171 if (remoteTransition == null) { 1172 logV("startLaunchTransition -- no remoteTransition -- wct = $launchTransaction") 1173 desktopMixedTransitionHandler.startLaunchTransition( 1174 transitionType = transitionType, 1175 wct = launchTransaction, 1176 taskId = launchingTaskId, 1177 minimizingTaskId = taskIdToMinimize, 1178 exitingImmersiveTask = exitImmersiveResult.asExit()?.exitingTask, 1179 ) 1180 } else if (taskIdToMinimize == null) { 1181 val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) 1182 transitions 1183 .startTransition(transitionType, launchTransaction, remoteTransitionHandler) 1184 .also { remoteTransitionHandler.setTransition(it) } 1185 } else { 1186 val remoteTransitionHandler = 1187 DesktopWindowLimitRemoteHandler( 1188 mainExecutor, 1189 rootTaskDisplayAreaOrganizer, 1190 remoteTransition, 1191 taskIdToMinimize, 1192 ) 1193 transitions 1194 .startTransition(transitionType, launchTransaction, remoteTransitionHandler) 1195 .also { remoteTransitionHandler.setTransition(it) } 1196 } 1197 if (taskIdToMinimize != null) { 1198 addPendingMinimizeTransition(t, taskIdToMinimize, MinimizeReason.TASK_LIMIT) 1199 } 1200 if (launchingTaskId != null && taskRepository.isMinimizedTask(launchingTaskId)) { 1201 addPendingUnminimizeTransition(t, displayId, launchingTaskId, unminimizeReason) 1202 } 1203 activationRunOnTransitStart?.invoke(t) 1204 exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t) 1205 return t 1206 } 1207 1208 /** 1209 * Move task to the next display. 1210 * 1211 * Queries all current known display ids and sorts them in ascending order. Then iterates 1212 * through the list and looks for the display id that is larger than the display id for the 1213 * passed in task. If a display with a higher id is not found, iterates through the list and 1214 * finds the first display id that is not the display id for the passed in task. 1215 * 1216 * If a display matching the above criteria is found, re-parents the task to that display. No-op 1217 * if no such display is found. 1218 */ 1219 fun moveToNextDisplay(taskId: Int) { 1220 val task = shellTaskOrganizer.getRunningTaskInfo(taskId) 1221 if (task == null) { 1222 logW("moveToNextDisplay: taskId=%d not found", taskId) 1223 return 1224 } 1225 logV("moveToNextDisplay: taskId=%d displayId=%d", taskId, task.displayId) 1226 1227 val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted() 1228 // Get the first display id that is higher than current task display id 1229 var newDisplayId = displayIds.firstOrNull { displayId -> displayId > task.displayId } 1230 if (newDisplayId == null) { 1231 // No display with a higher id, get the first display id that is not the task display id 1232 newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId } 1233 } 1234 if (newDisplayId == null) { 1235 logW("moveToNextDisplay: next display not found") 1236 return 1237 } 1238 moveToDisplay(task, newDisplayId) 1239 } 1240 1241 /** 1242 * Start an intent through a launch transition for starting tasks whose transition does not get 1243 * handled by [handleRequest] 1244 */ 1245 fun startLaunchIntentTransition(intent: Intent, options: Bundle, displayId: Int) { 1246 val wct = WindowContainerTransaction() 1247 val displayLayout = displayController.getDisplayLayout(displayId) ?: return 1248 val bounds = calculateDefaultDesktopTaskBounds(displayLayout) 1249 if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue) { 1250 cascadeWindow(bounds, displayLayout, displayId) 1251 } 1252 val pendingIntent = 1253 PendingIntent.getActivityAsUser( 1254 context, 1255 /* requestCode= */ 0, 1256 intent, 1257 PendingIntent.FLAG_IMMUTABLE, 1258 /* options= */ null, 1259 UserHandle.of(userId), 1260 ) 1261 val ops = 1262 ActivityOptions.fromBundle(options).apply { 1263 launchWindowingMode = WINDOWING_MODE_FREEFORM 1264 pendingIntentBackgroundActivityStartMode = 1265 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS 1266 launchBounds = bounds 1267 launchDisplayId = displayId 1268 if (DesktopModeFlags.ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX.isTrue) { 1269 // Sets launch bounds size as flexible so core can recalculate. 1270 flexibleLaunchSize = true 1271 } 1272 } 1273 1274 wct.sendPendingIntent(pendingIntent, intent, ops.toBundle()) 1275 val deskId = getDefaultDeskId(displayId) 1276 startLaunchTransition( 1277 TRANSIT_OPEN, 1278 wct, 1279 launchingTaskId = null, 1280 deskId = deskId, 1281 displayId = displayId, 1282 ) 1283 } 1284 1285 /** 1286 * Move [task] to display with [displayId]. 1287 * 1288 * No-op if task is already on that display per [RunningTaskInfo.displayId]. 1289 * 1290 * TODO: b/399411604 - split this up into smaller functions. 1291 */ 1292 private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) { 1293 logV("moveToDisplay: taskId=%d displayId=%d", task.taskId, displayId) 1294 if (task.displayId == displayId) { 1295 logD("moveToDisplay: task already on display %d", displayId) 1296 return 1297 } 1298 1299 if (splitScreenController.isTaskInSplitScreen(task.taskId)) { 1300 moveSplitPairToDisplay(task, displayId) 1301 return 1302 } 1303 1304 val wct = WindowContainerTransaction() 1305 val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) 1306 if (displayAreaInfo == null) { 1307 logW("moveToDisplay: display not found") 1308 return 1309 } 1310 1311 val destinationDeskId = taskRepository.getDefaultDeskId(displayId) 1312 if (destinationDeskId == null) { 1313 logW("moveToDisplay: desk not found for display: $displayId") 1314 return 1315 } 1316 1317 // TODO: b/393977830 and b/397437641 - do not assume that freeform==desktop. 1318 if (!task.isFreeform) { 1319 addMoveToDeskTaskChanges(wct = wct, task = task, deskId = destinationDeskId) 1320 } else { 1321 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 1322 desksOrganizer.moveTaskToDesk(wct, destinationDeskId, task) 1323 } 1324 if (Flags.enableMoveToNextDisplayShortcut()) { 1325 applyFreeformDisplayChange(wct, task, displayId) 1326 } 1327 } 1328 1329 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 1330 wct.reparent(task.token, displayAreaInfo.token, /* onTop= */ true) 1331 } 1332 1333 val activationRunnable = addDeskActivationChanges(destinationDeskId, wct, task) 1334 1335 if (Flags.enableDisplayFocusInShellTransitions()) { 1336 // Bring the destination display to top with includingParents=true, so that the 1337 // destination display gains the display focus, which makes the top task in the display 1338 // gains the global focus. 1339 wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) 1340 } 1341 1342 val sourceDisplayId = task.displayId 1343 val sourceDeskId = taskRepository.getDeskIdForTask(task.taskId) 1344 val shouldExitDesktopIfNeeded = 1345 Flags.enablePerDisplayDesktopWallpaperActivity() || 1346 DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue 1347 val deactivationRunnable = 1348 if (shouldExitDesktopIfNeeded) { 1349 performDesktopExitCleanupIfNeeded( 1350 taskId = task.taskId, 1351 deskId = sourceDeskId, 1352 displayId = sourceDisplayId, 1353 wct = wct, 1354 forceToFullscreen = false, 1355 // TODO: b/371096166 - Temporary turing home relaunch off to prevent home 1356 // stealing 1357 // display focus. Remove shouldEndUpAtHome = false when home focus handling 1358 // with connected display is implemented in wm core. 1359 shouldEndUpAtHome = false, 1360 ) 1361 } else { 1362 null 1363 } 1364 val transition = 1365 transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler) 1366 deactivationRunnable?.invoke(transition) 1367 activationRunnable?.invoke(transition) 1368 } 1369 1370 /** 1371 * Move split pair associated with the [task] to display with [displayId]. 1372 * 1373 * No-op if task is already on that display per [RunningTaskInfo.displayId]. 1374 */ 1375 private fun moveSplitPairToDisplay(task: RunningTaskInfo, displayId: Int) { 1376 if (!splitScreenController.isTaskInSplitScreen(task.taskId)) { 1377 return 1378 } 1379 1380 if (!Flags.enableNonDefaultDisplaySplit() || !Flags.enableMoveToNextDisplayShortcut()) { 1381 return 1382 } 1383 1384 val wct = WindowContainerTransaction() 1385 val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) 1386 if (displayAreaInfo == null) { 1387 logW("moveSplitPairToDisplay: display not found") 1388 return 1389 } 1390 1391 val activeDeskId = taskRepository.getActiveDeskId(displayId) 1392 logV("moveSplitPairToDisplay: moving split root to displayId=%d", displayId) 1393 1394 val stageCoordinatorRootTaskToken = 1395 splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(DEFAULT_DISPLAY) 1396 if (stageCoordinatorRootTaskToken == null) { 1397 return 1398 } 1399 wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */) 1400 1401 val deactivationRunnable = 1402 if (activeDeskId != null) { 1403 // Split is being placed on top of an existing desk in the target display. Make 1404 // sure it is cleaned up. 1405 performDesktopExitCleanUp( 1406 wct = wct, 1407 deskId = activeDeskId, 1408 displayId = displayId, 1409 willExitDesktop = true, 1410 shouldEndUpAtHome = false, 1411 ) 1412 } else { 1413 null 1414 } 1415 val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) 1416 deactivationRunnable?.invoke(transition) 1417 return 1418 } 1419 1420 /** 1421 * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable 1422 * bounds) and a free floating state (either the last saved bounds if available or the default 1423 * bounds otherwise). 1424 */ 1425 fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo, interaction: ToggleTaskSizeInteraction) { 1426 val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds 1427 desktopModeEventLogger.logTaskResizingStarted( 1428 interaction.resizeTrigger, 1429 interaction.inputMethod, 1430 taskInfo, 1431 currentTaskBounds.width(), 1432 currentTaskBounds.height(), 1433 displayController, 1434 ) 1435 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return 1436 val destinationBounds = Rect() 1437 val isMaximized = interaction.direction == ToggleTaskSizeInteraction.Direction.RESTORE 1438 // If the task is currently maximized, we will toggle it not to be and vice versa. This is 1439 // helpful to eliminate the current task from logic to calculate taskbar corner rounding. 1440 val willMaximize = interaction.direction == ToggleTaskSizeInteraction.Direction.MAXIMIZE 1441 if (isMaximized) { 1442 // The desktop task is at the maximized width and/or height of the stable bounds. 1443 // If the task's pre-maximize stable bounds were saved, toggle the task to those bounds. 1444 // Otherwise, toggle to the default bounds. 1445 val taskBoundsBeforeMaximize = 1446 taskRepository.removeBoundsBeforeMaximize(taskInfo.taskId) 1447 if (taskBoundsBeforeMaximize != null) { 1448 destinationBounds.set(taskBoundsBeforeMaximize) 1449 } else { 1450 if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) { 1451 destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo)) 1452 } else { 1453 destinationBounds.set(calculateDefaultDesktopTaskBounds(displayLayout)) 1454 } 1455 } 1456 } else { 1457 // Save current bounds so that task can be restored back to original bounds if necessary 1458 // and toggle to the stable bounds. 1459 snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) 1460 taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds) 1461 destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo)) 1462 } 1463 1464 val shouldRestoreToSnap = 1465 isMaximized && isTaskSnappedToHalfScreen(taskInfo, destinationBounds) 1466 1467 logD("willMaximize = %s", willMaximize) 1468 logD("shouldRestoreToSnap = %s", shouldRestoreToSnap) 1469 1470 val doesAnyTaskRequireTaskbarRounding = 1471 willMaximize || 1472 shouldRestoreToSnap || 1473 doesAnyTaskRequireTaskbarRounding(taskInfo.displayId, taskInfo.taskId) 1474 1475 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding) 1476 val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) 1477 interaction.uiEvent?.let { uiEvent -> desktopModeUiEventLogger.log(taskInfo, uiEvent) } 1478 desktopModeEventLogger.logTaskResizingEnded( 1479 interaction.resizeTrigger, 1480 interaction.inputMethod, 1481 taskInfo, 1482 destinationBounds.width(), 1483 destinationBounds.height(), 1484 displayController, 1485 ) 1486 toggleResizeDesktopTaskTransitionHandler.startTransition( 1487 wct, 1488 interaction.animationStartBounds, 1489 ) 1490 } 1491 1492 private fun dragToMaximizeDesktopTask( 1493 taskInfo: RunningTaskInfo, 1494 taskSurface: SurfaceControl, 1495 currentDragBounds: Rect, 1496 motionEvent: MotionEvent, 1497 ) { 1498 if (isTaskMaximized(taskInfo, displayController)) { 1499 // Handle the case where we attempt to drag-to-maximize when already maximized: the task 1500 // position won't need to change but we want to animate the surface going back to the 1501 // maximized position. 1502 val containerBounds = taskInfo.configuration.windowConfiguration.bounds 1503 if (containerBounds != currentDragBounds) { 1504 returnToDragStartAnimator.start( 1505 taskInfo.taskId, 1506 taskSurface, 1507 startBounds = currentDragBounds, 1508 endBounds = containerBounds, 1509 ) 1510 } 1511 return 1512 } 1513 1514 toggleDesktopTaskSize( 1515 taskInfo, 1516 ToggleTaskSizeInteraction( 1517 direction = ToggleTaskSizeInteraction.Direction.MAXIMIZE, 1518 source = ToggleTaskSizeInteraction.Source.HEADER_DRAG_TO_TOP, 1519 inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent), 1520 animationStartBounds = currentDragBounds, 1521 ), 1522 ) 1523 } 1524 1525 private fun isMaximizedToStableBoundsEdges( 1526 taskInfo: RunningTaskInfo, 1527 stableBounds: Rect, 1528 ): Boolean { 1529 val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds 1530 return isTaskBoundsEqual(currentTaskBounds, stableBounds) 1531 } 1532 1533 /** Returns if current task bound is snapped to half screen */ 1534 private fun isTaskSnappedToHalfScreen( 1535 taskInfo: RunningTaskInfo, 1536 taskBounds: Rect = taskInfo.configuration.windowConfiguration.bounds, 1537 ): Boolean = 1538 getSnapBounds(taskInfo, SnapPosition.LEFT) == taskBounds || 1539 getSnapBounds(taskInfo, SnapPosition.RIGHT) == taskBounds 1540 1541 @VisibleForTesting 1542 fun doesAnyTaskRequireTaskbarRounding(displayId: Int, excludeTaskId: Int? = null): Boolean { 1543 val doesAnyTaskRequireTaskbarRounding = 1544 taskRepository 1545 .getExpandedTasksOrdered(displayId) 1546 // exclude current task since maximize/restore transition has not taken place yet. 1547 .filterNot { taskId -> taskId == excludeTaskId } 1548 .any { taskId -> 1549 val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false 1550 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) 1551 val stableBounds = Rect().apply { displayLayout?.getStableBounds(this) } 1552 logD("taskInfo = %s", taskInfo) 1553 logD( 1554 "isTaskSnappedToHalfScreen(taskInfo) = %s", 1555 isTaskSnappedToHalfScreen(taskInfo), 1556 ) 1557 logD( 1558 "isMaximizedToStableBoundsEdges(taskInfo, stableBounds) = %s", 1559 isMaximizedToStableBoundsEdges(taskInfo, stableBounds), 1560 ) 1561 isTaskSnappedToHalfScreen(taskInfo) || 1562 isMaximizedToStableBoundsEdges(taskInfo, stableBounds) 1563 } 1564 1565 logD("doesAnyTaskRequireTaskbarRounding = %s", doesAnyTaskRequireTaskbarRounding) 1566 return doesAnyTaskRequireTaskbarRounding 1567 } 1568 1569 /** 1570 * Quick-resize to the right or left half of the stable bounds. 1571 * 1572 * @param taskInfo current task that is being snap-resized via dragging or maximize menu button 1573 * @param taskSurface the leash of the task being dragged 1574 * @param currentDragBounds current position of the task leash being dragged (or current task 1575 * bounds if being snapped resize via maximize menu button) 1576 * @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to. 1577 */ 1578 fun snapToHalfScreen( 1579 taskInfo: RunningTaskInfo, 1580 taskSurface: SurfaceControl?, 1581 currentDragBounds: Rect, 1582 position: SnapPosition, 1583 resizeTrigger: ResizeTrigger, 1584 inputMethod: InputMethod, 1585 ) { 1586 desktopModeEventLogger.logTaskResizingStarted( 1587 resizeTrigger, 1588 inputMethod, 1589 taskInfo, 1590 currentDragBounds.width(), 1591 currentDragBounds.height(), 1592 displayController, 1593 ) 1594 1595 val destinationBounds = getSnapBounds(taskInfo, position) 1596 desktopModeEventLogger.logTaskResizingEnded( 1597 resizeTrigger, 1598 inputMethod, 1599 taskInfo, 1600 destinationBounds.width(), 1601 destinationBounds.height(), 1602 displayController, 1603 ) 1604 1605 if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) { 1606 val isTiled = snapEventHandler.snapToHalfScreen(taskInfo, currentDragBounds, position) 1607 if (isTiled) { 1608 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true) 1609 } 1610 return 1611 } 1612 1613 if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) { 1614 // Handle the case where we attempt to snap resize when already snap resized: the task 1615 // position won't need to change but we want to animate the surface going back to the 1616 // snapped position from the "dragged-to-the-edge" position. 1617 if (destinationBounds != currentDragBounds && taskSurface != null) { 1618 returnToDragStartAnimator.start( 1619 taskInfo.taskId, 1620 taskSurface, 1621 startBounds = currentDragBounds, 1622 endBounds = destinationBounds, 1623 ) 1624 } 1625 return 1626 } 1627 1628 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true) 1629 val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) 1630 1631 toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds) 1632 } 1633 1634 /** 1635 * Handles snap resizing a [taskInfo] to [position] instantaneously, for example when the 1636 * [resizeTrigger] is the snap resize menu using any [motionEvent] or a keyboard shortcut. 1637 */ 1638 fun handleInstantSnapResizingTask( 1639 taskInfo: RunningTaskInfo, 1640 position: SnapPosition, 1641 resizeTrigger: ResizeTrigger, 1642 inputMethod: InputMethod, 1643 ) { 1644 if (!isSnapResizingAllowed(taskInfo)) { 1645 Toast.makeText( 1646 getContext(), 1647 R.string.desktop_mode_non_resizable_snap_text, 1648 Toast.LENGTH_SHORT, 1649 ) 1650 .show() 1651 return 1652 } 1653 1654 snapToHalfScreen( 1655 taskInfo, 1656 null, 1657 taskInfo.configuration.windowConfiguration.bounds, 1658 position, 1659 resizeTrigger, 1660 inputMethod, 1661 ) 1662 } 1663 1664 @VisibleForTesting 1665 fun handleSnapResizingTaskOnDrag( 1666 taskInfo: RunningTaskInfo, 1667 position: SnapPosition, 1668 taskSurface: SurfaceControl, 1669 currentDragBounds: Rect, 1670 dragStartBounds: Rect, 1671 motionEvent: MotionEvent, 1672 ) { 1673 releaseVisualIndicator() 1674 if (!isSnapResizingAllowed(taskInfo)) { 1675 interactionJankMonitor.begin( 1676 taskSurface, 1677 context, 1678 handler, 1679 CUJ_DESKTOP_MODE_SNAP_RESIZE, 1680 "drag_non_resizable", 1681 ) 1682 1683 // reposition non-resizable app back to its original position before being dragged 1684 returnToDragStartAnimator.start( 1685 taskInfo.taskId, 1686 taskSurface, 1687 startBounds = currentDragBounds, 1688 endBounds = dragStartBounds, 1689 doOnEnd = { 1690 Toast.makeText( 1691 context, 1692 com.android.wm.shell.R.string.desktop_mode_non_resizable_snap_text, 1693 Toast.LENGTH_SHORT, 1694 ) 1695 .show() 1696 }, 1697 ) 1698 } else { 1699 val resizeTrigger = 1700 if (position == SnapPosition.LEFT) { 1701 ResizeTrigger.DRAG_LEFT 1702 } else { 1703 ResizeTrigger.DRAG_RIGHT 1704 } 1705 interactionJankMonitor.begin( 1706 taskSurface, 1707 context, 1708 handler, 1709 CUJ_DESKTOP_MODE_SNAP_RESIZE, 1710 "drag_resizable", 1711 ) 1712 snapToHalfScreen( 1713 taskInfo, 1714 taskSurface, 1715 currentDragBounds, 1716 position, 1717 resizeTrigger, 1718 DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent), 1719 ) 1720 } 1721 } 1722 1723 private fun isSnapResizingAllowed(taskInfo: RunningTaskInfo) = 1724 taskInfo.isResizeable || !DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue() 1725 1726 private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect { 1727 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect() 1728 1729 val stableBounds = Rect() 1730 displayLayout.getStableBounds(stableBounds) 1731 1732 val destinationWidth = stableBounds.width() / 2 1733 return when (position) { 1734 SnapPosition.LEFT -> { 1735 Rect( 1736 stableBounds.left, 1737 stableBounds.top, 1738 stableBounds.left + destinationWidth, 1739 stableBounds.bottom, 1740 ) 1741 } 1742 SnapPosition.RIGHT -> { 1743 Rect( 1744 stableBounds.right - destinationWidth, 1745 stableBounds.top, 1746 stableBounds.right, 1747 stableBounds.bottom, 1748 ) 1749 } 1750 } 1751 } 1752 1753 /** 1754 * Get windowing move for a given `taskId` 1755 * 1756 * @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found 1757 */ 1758 @WindowingMode 1759 fun getTaskWindowingMode(taskId: Int): Int { 1760 return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode 1761 ?: WINDOWING_MODE_UNDEFINED 1762 } 1763 1764 private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) { 1765 // Move home to front, ensures that we go back home when all desktop windows are closed 1766 val useParamDisplayId = 1767 DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue || 1768 Flags.enablePerDisplayDesktopWallpaperActivity() 1769 moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct) 1770 // Currently, we only handle the desktop on the default display really. 1771 if ( 1772 (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) && 1773 ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() 1774 ) { 1775 // Add translucent wallpaper activity to show the wallpaper underneath. 1776 addWallpaperActivity(displayId, wct) 1777 } 1778 } 1779 1780 @Deprecated( 1781 "Use addDeskActivationChanges() instead.", 1782 ReplaceWith("addDeskActivationChanges()"), 1783 ) 1784 private fun bringDesktopAppsToFront( 1785 displayId: Int, 1786 wct: WindowContainerTransaction, 1787 newTaskIdInFront: Int? = null, 1788 ): Int? { 1789 logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront) 1790 prepareForDeskActivation(displayId, wct) 1791 1792 val expandedTasksOrderedFrontToBack = taskRepository.getExpandedTasksOrdered(displayId) 1793 // If we're adding a new Task we might need to minimize an old one 1794 // TODO(b/365725441): Handle non running task minimization 1795 val taskIdToMinimize: Int? = 1796 if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { 1797 desktopTasksLimiter 1798 .get() 1799 .getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront) 1800 } else { 1801 null 1802 } 1803 1804 expandedTasksOrderedFrontToBack 1805 // If there is a Task to minimize, let it stay behind the Home Task 1806 .filter { taskId -> taskId != taskIdToMinimize } 1807 .reversed() // Start from the back so the front task is brought forward last 1808 .forEach { taskId -> 1809 val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) 1810 if (runningTaskInfo != null) { 1811 // Task is already running, reorder it to the front 1812 wct.reorder(runningTaskInfo.token, /* onTop= */ true) 1813 } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { 1814 // Task is not running, start it 1815 wct.startTask(taskId, createActivityOptionsForStartTask().toBundle()) 1816 } 1817 } 1818 1819 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( 1820 doesAnyTaskRequireTaskbarRounding(displayId) 1821 ) 1822 1823 return taskIdToMinimize 1824 } 1825 1826 private fun moveHomeTask(displayId: Int, wct: WindowContainerTransaction) { 1827 shellTaskOrganizer 1828 .getRunningTasks(displayId) 1829 .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } 1830 ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ true) } 1831 } 1832 1833 private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) { 1834 homeIntentProvider.addLaunchHomePendingIntent(wct, displayId, userId) 1835 } 1836 1837 private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { 1838 logV("addWallpaperActivity") 1839 if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { 1840 1841 // If the wallpaper activity for this display already exists, let's reorder it to top. 1842 val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId) 1843 if (wallpaperActivityToken != null) { 1844 wct.reorder(wallpaperActivityToken, /* onTop= */ true) 1845 return 1846 } 1847 1848 val intent = Intent(context, DesktopWallpaperActivity::class.java) 1849 if (Flags.enablePerDisplayDesktopWallpaperActivity()) { 1850 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 1851 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 1852 } 1853 val options = 1854 ActivityOptions.makeBasic().apply { 1855 launchWindowingMode = WINDOWING_MODE_FULLSCREEN 1856 pendingIntentBackgroundActivityStartMode = 1857 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS 1858 if (Flags.enablePerDisplayDesktopWallpaperActivity()) { 1859 launchDisplayId = displayId 1860 } 1861 } 1862 val pendingIntent = 1863 PendingIntent.getActivity( 1864 context, 1865 /* requestCode = */ 0, 1866 intent, 1867 PendingIntent.FLAG_IMMUTABLE, 1868 ) 1869 wct.sendPendingIntent(pendingIntent, intent, options.toBundle()) 1870 } else { 1871 val userHandle = UserHandle.of(userId) 1872 val userContext = context.createContextAsUser(userHandle, /* flags= */ 0) 1873 val intent = Intent(userContext, DesktopWallpaperActivity::class.java) 1874 if ( 1875 desktopWallpaperActivityTokenProvider.getToken(displayId) == null && 1876 Flags.enablePerDisplayDesktopWallpaperActivity() 1877 ) { 1878 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 1879 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 1880 } 1881 intent.putExtra(Intent.EXTRA_USER_HANDLE, userId) 1882 val options = 1883 ActivityOptions.makeBasic().apply { 1884 launchWindowingMode = WINDOWING_MODE_FULLSCREEN 1885 pendingIntentBackgroundActivityStartMode = 1886 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS 1887 if (Flags.enablePerDisplayDesktopWallpaperActivity()) { 1888 launchDisplayId = displayId 1889 } 1890 } 1891 val pendingIntent = 1892 PendingIntent.getActivityAsUser( 1893 userContext, 1894 /* requestCode= */ 0, 1895 intent, 1896 PendingIntent.FLAG_IMMUTABLE, 1897 /* options= */ null, 1898 userHandle, 1899 ) 1900 wct.sendPendingIntent(pendingIntent, intent, options.toBundle()) 1901 } 1902 } 1903 1904 private fun removeWallpaperActivity(wct: WindowContainerTransaction, displayId: Int) { 1905 desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token -> 1906 logV("removeWallpaperActivity") 1907 if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { 1908 wct.reorder(token, /* onTop= */ false) 1909 } else { 1910 wct.removeTask(token) 1911 } 1912 } 1913 } 1914 1915 private fun willExitDesktop( 1916 triggerTaskId: Int, 1917 displayId: Int, 1918 forceExitDesktop: Boolean, 1919 ): Boolean { 1920 if (forceExitDesktop && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 1921 // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when 1922 // explicitly going fullscreen, so there's no point in checking the desktop state. 1923 return true 1924 } 1925 if (Flags.enablePerDisplayDesktopWallpaperActivity()) { 1926 if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) { 1927 return false 1928 } 1929 } else { 1930 if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId)) { 1931 return false 1932 } 1933 } 1934 return true 1935 } 1936 1937 private fun performDesktopExitCleanupIfNeeded( 1938 taskId: Int, 1939 deskId: Int? = null, 1940 displayId: Int, 1941 wct: WindowContainerTransaction, 1942 forceToFullscreen: Boolean, 1943 shouldEndUpAtHome: Boolean = true, 1944 ): RunOnTransitStart? { 1945 if (!willExitDesktop(taskId, displayId, forceToFullscreen)) { 1946 return null 1947 } 1948 // TODO: b/394268248 - update remaining callers to pass in a |deskId| and apply the 1949 // |RunOnTransitStart| when the transition is started. 1950 return performDesktopExitCleanUp( 1951 wct = wct, 1952 deskId = deskId, 1953 displayId = displayId, 1954 willExitDesktop = true, 1955 shouldEndUpAtHome = shouldEndUpAtHome, 1956 ) 1957 } 1958 1959 /** TODO: b/394268248 - update [deskId] to be non-null. */ 1960 fun performDesktopExitCleanUp( 1961 wct: WindowContainerTransaction, 1962 deskId: Int?, 1963 displayId: Int, 1964 willExitDesktop: Boolean, 1965 shouldEndUpAtHome: Boolean = true, 1966 fromRecentsTransition: Boolean = false, 1967 ): RunOnTransitStart? { 1968 if (!willExitDesktop) return null 1969 desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( 1970 FULLSCREEN_ANIMATION_DURATION 1971 ) 1972 // No need to clean up the wallpaper / reorder home when coming from a recents transition. 1973 if ( 1974 !fromRecentsTransition || 1975 !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue 1976 ) { 1977 removeWallpaperActivity(wct, displayId) 1978 if (shouldEndUpAtHome) { 1979 // If the transition should end up with user going to home, launch home with a 1980 // pending intent. 1981 addLaunchHomePendingIntent(wct, displayId) 1982 } 1983 } 1984 return prepareDeskDeactivationIfNeeded(wct, deskId) 1985 } 1986 1987 fun releaseVisualIndicator() { 1988 visualIndicator?.releaseVisualIndicator() 1989 visualIndicator = null 1990 } 1991 1992 override fun getContext(): Context = context 1993 1994 override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor 1995 1996 override fun startAnimation( 1997 transition: IBinder, 1998 info: TransitionInfo, 1999 startTransaction: SurfaceControl.Transaction, 2000 finishTransaction: SurfaceControl.Transaction, 2001 finishCallback: Transitions.TransitionFinishCallback, 2002 ): Boolean { 2003 // This handler should never be the sole handler, so should not animate anything. 2004 return false 2005 } 2006 2007 override fun handleRequest( 2008 transition: IBinder, 2009 request: TransitionRequestInfo, 2010 ): WindowContainerTransaction? { 2011 logV("handleRequest request=%s", request) 2012 // Check if we should skip handling this transition 2013 var reason = "" 2014 val triggerTask = request.triggerTask 2015 val recentsAnimationRunning = 2016 RecentsTransitionStateListener.isAnimating(recentsTransitionState) 2017 var shouldHandleMidRecentsFreeformLaunch = 2018 recentsAnimationRunning && isFreeformRelaunch(triggerTask, request) 2019 val isDragAndDropFullscreenTransition = taskContainsDragAndDropCookie(triggerTask) 2020 val shouldHandleRequest = 2021 when { 2022 // Handle freeform relaunch during recents animation 2023 shouldHandleMidRecentsFreeformLaunch -> true 2024 recentsAnimationRunning -> { 2025 reason = "recents animation is running" 2026 false 2027 } 2028 // Don't handle request if this was a tear to fullscreen transition. 2029 // handleFullscreenTaskLaunch moves fullscreen intents to freeform; 2030 // this is an exception to the rule 2031 isDragAndDropFullscreenTransition -> { 2032 dragAndDropFullscreenCookie = null 2033 false 2034 } 2035 // Handle task closing for the last window if wallpaper is available 2036 shouldHandleTaskClosing(request) -> true 2037 // Only handle open or to front transitions 2038 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> { 2039 reason = "transition type not handled (${request.type})" 2040 false 2041 } 2042 // Only handle when it is a task transition 2043 triggerTask == null -> { 2044 reason = "triggerTask is null" 2045 false 2046 } 2047 // Only handle standard type tasks 2048 triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> { 2049 reason = "activityType not handled (${triggerTask.activityType})" 2050 false 2051 } 2052 // Only handle fullscreen or freeform tasks 2053 !triggerTask.isFullscreen && !triggerTask.isFreeform -> { 2054 reason = "windowingMode not handled (${triggerTask.windowingMode})" 2055 false 2056 } 2057 // Otherwise process it 2058 else -> true 2059 } 2060 2061 if (!shouldHandleRequest) { 2062 logV("skipping handleRequest reason=%s", reason) 2063 return null 2064 } 2065 2066 val result = 2067 triggerTask?.let { task -> 2068 when { 2069 // Check if freeform task launch during recents should be handled 2070 shouldHandleMidRecentsFreeformLaunch -> 2071 handleMidRecentsFreeformTaskLaunch(task, transition) 2072 // Check if the closing task needs to be handled 2073 TransitionUtil.isClosingType(request.type) -> 2074 handleTaskClosing(task, transition, request.type) 2075 // Check if the top task shouldn't be allowed to enter desktop mode 2076 isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task, transition) 2077 // Check if fullscreen task should be updated 2078 task.isFullscreen -> handleFullscreenTaskLaunch(task, transition) 2079 // Check if freeform task should be updated 2080 task.isFreeform -> handleFreeformTaskLaunch(task, transition) 2081 else -> { 2082 null 2083 } 2084 } 2085 } 2086 logV("handleRequest result=%s", result) 2087 return result 2088 } 2089 2090 /** Whether the given [change] in the [transition] is a known desktop change. */ 2091 fun isDesktopChange(transition: IBinder, change: TransitionInfo.Change): Boolean { 2092 // Only the immersive controller is currently involved in mixed transitions. 2093 return DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue && 2094 desktopImmersiveController.isImmersiveChange(transition, change) 2095 } 2096 2097 /** 2098 * Whether the given transition [info] will potentially include a desktop change, in which case 2099 * the transition should be treated as mixed so that the change is in part animated by one of 2100 * the desktop transition handlers. 2101 */ 2102 fun shouldPlayDesktopAnimation(info: TransitionRequestInfo): Boolean { 2103 // Only immersive mixed transition are currently supported. 2104 if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return false 2105 val triggerTask = info.triggerTask ?: return false 2106 if (!isDesktopModeShowing(triggerTask.displayId)) { 2107 return false 2108 } 2109 if (!TransitionUtil.isOpeningType(info.type)) { 2110 return false 2111 } 2112 taskRepository.getTaskInFullImmersiveState(displayId = triggerTask.displayId) 2113 ?: return false 2114 return when { 2115 triggerTask.isFullscreen -> { 2116 // Trigger fullscreen task will enter desktop, so any existing immersive task 2117 // should exit. 2118 shouldFullscreenTaskLaunchSwitchToDesktop(triggerTask) 2119 } 2120 triggerTask.isFreeform -> { 2121 // Trigger freeform task will enter desktop, so any existing immersive task should 2122 // exit. 2123 !shouldFreeformTaskLaunchSwitchToFullscreen(triggerTask) 2124 } 2125 else -> false 2126 } 2127 } 2128 2129 /** Animate a desktop change found in a mixed transitions. */ 2130 fun animateDesktopChange( 2131 transition: IBinder, 2132 change: Change, 2133 startTransaction: Transaction, 2134 finishTransaction: Transaction, 2135 finishCallback: TransitionFinishCallback, 2136 ) { 2137 if (!desktopImmersiveController.isImmersiveChange(transition, change)) { 2138 throw IllegalStateException("Only immersive changes support desktop mixed transitions") 2139 } 2140 desktopImmersiveController.animateResizeChange( 2141 change, 2142 startTransaction, 2143 finishTransaction, 2144 finishCallback, 2145 ) 2146 } 2147 2148 private fun taskContainsDragAndDropCookie(taskInfo: RunningTaskInfo?) = 2149 taskInfo?.launchCookies?.any { it == dragAndDropFullscreenCookie } ?: false 2150 2151 /** 2152 * Applies the proper surface states (rounded corners) to tasks when desktop mode is active. 2153 * This is intended to be used when desktop mode is part of another animation but isn't, itself, 2154 * animating. 2155 */ 2156 fun syncSurfaceState(info: TransitionInfo, finishTransaction: SurfaceControl.Transaction) { 2157 // Add rounded corners to freeform windows 2158 if (!DesktopModeStatus.useRoundedCorners()) { 2159 return 2160 } 2161 val cornerRadius = 2162 context.resources 2163 .getDimensionPixelSize( 2164 SharedR.dimen.desktop_windowing_freeform_rounded_corner_radius 2165 ) 2166 .toFloat() 2167 info.changes 2168 .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM } 2169 .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } 2170 } 2171 2172 /** Returns whether an existing desktop task is being relaunched in freeform or not. */ 2173 private fun isFreeformRelaunch(triggerTask: RunningTaskInfo?, request: TransitionRequestInfo) = 2174 (triggerTask != null && 2175 triggerTask.windowingMode == WINDOWING_MODE_FREEFORM && 2176 TransitionUtil.isOpeningType(request.type) && 2177 taskRepository.isActiveTask(triggerTask.taskId)) 2178 2179 private fun isIncompatibleTask(task: RunningTaskInfo) = 2180 desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task) 2181 2182 private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean = 2183 ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && 2184 TransitionUtil.isClosingType(request.type) && 2185 request.triggerTask != null 2186 2187 /** Open an existing instance of an app. */ 2188 fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) { 2189 if (callingTask.isFreeform) { 2190 val requestedTaskInfo = shellTaskOrganizer.getRunningTaskInfo(requestedTaskId) 2191 if (requestedTaskInfo?.isFreeform == true) { 2192 // If requested task is an already open freeform task, just move it to front. 2193 moveTaskToFront( 2194 requestedTaskId, 2195 unminimizeReason = UnminimizeReason.APP_HANDLE_MENU_BUTTON, 2196 ) 2197 } else { 2198 val deskId = getDefaultDeskId(callingTask.displayId) 2199 moveTaskToDesk( 2200 requestedTaskId, 2201 deskId, 2202 WindowContainerTransaction(), 2203 DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON, 2204 ) 2205 } 2206 } else { 2207 val options = createNewWindowOptions(callingTask) 2208 val splitPosition = splitScreenController.determineNewInstancePosition(callingTask) 2209 splitScreenController.startTask( 2210 requestedTaskId, 2211 splitPosition, 2212 options.toBundle(), 2213 /* hideTaskToken= */ null, 2214 if (enableFlexibleSplit()) 2215 splitScreenController.determineNewInstanceIndex(callingTask) 2216 else SPLIT_INDEX_UNDEFINED, 2217 ) 2218 } 2219 } 2220 2221 /** Create an Intent to open a new window of a task. */ 2222 fun openNewWindow(callingTaskInfo: RunningTaskInfo) { 2223 // TODO(b/337915660): Add a transition handler for these; animations 2224 // need updates in some cases. 2225 val baseActivity = callingTaskInfo.baseActivity ?: return 2226 val userHandle = UserHandle.of(callingTaskInfo.userId) 2227 val fillIn: Intent = 2228 userProfileContexts 2229 .getOrCreate(callingTaskInfo.userId) 2230 .packageManager 2231 .getLaunchIntentForPackage(baseActivity.packageName) ?: return 2232 fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 2233 val launchIntent = 2234 PendingIntent.getActivityAsUser( 2235 context, 2236 /* requestCode= */ 0, 2237 fillIn, 2238 PendingIntent.FLAG_IMMUTABLE, 2239 /* options= */ null, 2240 userHandle, 2241 ) 2242 val options = createNewWindowOptions(callingTaskInfo) 2243 when (options.launchWindowingMode) { 2244 WINDOWING_MODE_MULTI_WINDOW -> { 2245 val splitPosition = 2246 splitScreenController.determineNewInstancePosition(callingTaskInfo) 2247 // TODO(b/349828130) currently pass in index_undefined until we can revisit these 2248 // specific cases in the future. 2249 val splitIndex = 2250 if (enableFlexibleSplit()) 2251 splitScreenController.determineNewInstanceIndex(callingTaskInfo) 2252 else SPLIT_INDEX_UNDEFINED 2253 splitScreenController.startIntent( 2254 launchIntent, 2255 context.userId, 2256 fillIn, 2257 splitPosition, 2258 options.toBundle(), 2259 /* hideTaskToken= */ null, 2260 /* forceLaunchNewTask= */ true, 2261 splitIndex, 2262 ) 2263 } 2264 WINDOWING_MODE_FREEFORM -> { 2265 val wct = WindowContainerTransaction() 2266 wct.sendPendingIntent(launchIntent, fillIn, options.toBundle()) 2267 val deskId = 2268 taskRepository.getDeskIdForTask(callingTaskInfo.taskId) 2269 ?: getDefaultDeskId(callingTaskInfo.displayId) 2270 startLaunchTransition( 2271 transitionType = TRANSIT_OPEN, 2272 wct = wct, 2273 launchingTaskId = null, 2274 deskId = deskId, 2275 displayId = callingTaskInfo.displayId, 2276 ) 2277 } 2278 } 2279 } 2280 2281 private fun createNewWindowOptions(callingTask: RunningTaskInfo): ActivityOptions { 2282 val newTaskWindowingMode = 2283 when { 2284 callingTask.isFreeform -> { 2285 WINDOWING_MODE_FREEFORM 2286 } 2287 callingTask.isFullscreen || callingTask.isMultiWindow -> { 2288 WINDOWING_MODE_MULTI_WINDOW 2289 } 2290 else -> { 2291 error("Invalid windowing mode: ${callingTask.windowingMode}") 2292 } 2293 } 2294 val bounds = 2295 when (newTaskWindowingMode) { 2296 WINDOWING_MODE_FREEFORM -> { 2297 displayController.getDisplayLayout(callingTask.displayId)?.let { 2298 getInitialBounds(it, callingTask, callingTask.displayId) 2299 } 2300 } 2301 WINDOWING_MODE_MULTI_WINDOW -> { 2302 Rect() 2303 } 2304 else -> { 2305 error("Invalid windowing mode: $newTaskWindowingMode") 2306 } 2307 } 2308 return ActivityOptions.makeBasic().apply { 2309 launchWindowingMode = newTaskWindowingMode 2310 pendingIntentBackgroundActivityStartMode = 2311 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS 2312 launchBounds = bounds 2313 } 2314 } 2315 2316 /** 2317 * Handles the case where a freeform task is launched from recents. 2318 * 2319 * This is a special case where we want to launch the task in fullscreen instead of freeform. 2320 */ 2321 private fun handleMidRecentsFreeformTaskLaunch( 2322 task: RunningTaskInfo, 2323 transition: IBinder, 2324 ): WindowContainerTransaction? { 2325 logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch") 2326 val wct = WindowContainerTransaction() 2327 val runOnTransitStart = 2328 addMoveToFullscreenChanges( 2329 wct = wct, 2330 taskInfo = task, 2331 willExitDesktop = 2332 willExitDesktop( 2333 triggerTaskId = task.taskId, 2334 displayId = task.displayId, 2335 forceExitDesktop = true, 2336 ), 2337 ) 2338 runOnTransitStart?.invoke(transition) 2339 wct.reorder(task.token, true) 2340 return wct 2341 } 2342 2343 private fun handleFreeformTaskLaunch( 2344 task: RunningTaskInfo, 2345 transition: IBinder, 2346 ): WindowContainerTransaction? { 2347 logV("handleFreeformTaskLaunch") 2348 if (keyguardManager.isKeyguardLocked) { 2349 // Do NOT handle freeform task launch when locked. 2350 // It will be launched in fullscreen windowing mode (Details: b/160925539) 2351 logV("skip keyguard is locked") 2352 return null 2353 } 2354 val deskId = getDefaultDeskId(task.displayId) 2355 val wct = WindowContainerTransaction() 2356 if (shouldFreeformTaskLaunchSwitchToFullscreen(task)) { 2357 logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId) 2358 if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) { 2359 // We are outside of desktop mode and already existing desktop task is being 2360 // launched. We should make this task go to fullscreen instead of freeform. Note 2361 // that this means any re-launch of a freeform window outside of desktop will be in 2362 // fullscreen as long as default-desktop flag is disabled. 2363 val runOnTransitStart = 2364 addMoveToFullscreenChanges( 2365 wct = wct, 2366 taskInfo = task, 2367 willExitDesktop = 2368 willExitDesktop( 2369 triggerTaskId = task.taskId, 2370 displayId = task.displayId, 2371 forceExitDesktop = true, 2372 ), 2373 ) 2374 runOnTransitStart?.invoke(transition) 2375 return wct 2376 } 2377 val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) 2378 runOnTransitStart?.invoke(transition) 2379 wct.reorder(task.token, true) 2380 return wct 2381 } 2382 val inheritedTaskBounds = 2383 getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId) 2384 if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) { 2385 // Inherit bounds from closing task instance to prevent application jumping different 2386 // cascading positions. 2387 wct.setBounds(task.token, inheritedTaskBounds) 2388 } 2389 // TODO(b/365723620): Handle non running tasks that were launched after reboot. 2390 // If task is already visible, it must have been handled already and added to desktop mode. 2391 // Cascade task only if it's not visible yet and has no inherited bounds. 2392 if ( 2393 inheritedTaskBounds == null && 2394 DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() && 2395 !taskRepository.isVisibleTask(task.taskId) 2396 ) { 2397 val displayLayout = displayController.getDisplayLayout(task.displayId) 2398 if (displayLayout != null) { 2399 val initialBounds = Rect(task.configuration.windowConfiguration.bounds) 2400 cascadeWindow(initialBounds, displayLayout, task.displayId) 2401 wct.setBounds(task.token, initialBounds) 2402 } 2403 } 2404 if (useDesktopOverrideDensity()) { 2405 wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) 2406 } 2407 // The task that is launching might have been minimized before - in which case this is an 2408 // unminimize action. 2409 if (taskRepository.isMinimizedTask(task.taskId)) { 2410 addPendingUnminimizeTransition( 2411 transition, 2412 task.displayId, 2413 task.taskId, 2414 UnminimizeReason.TASK_LAUNCH, 2415 ) 2416 } 2417 // Desktop Mode is showing and we're launching a new Task: 2418 // 1) Exit immersive if needed. 2419 desktopImmersiveController.exitImmersiveIfApplicable( 2420 transition = transition, 2421 wct = wct, 2422 displayId = task.displayId, 2423 reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, 2424 ) 2425 // 2) minimize a Task if needed. 2426 val taskIdToMinimize = addAndGetMinimizeChanges(deskId, wct, task.taskId) 2427 addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) 2428 if (taskIdToMinimize != null) { 2429 addPendingMinimizeTransition(transition, taskIdToMinimize, MinimizeReason.TASK_LIMIT) 2430 return wct 2431 } 2432 if (!wct.isEmpty) { 2433 snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) 2434 return wct 2435 } 2436 return null 2437 } 2438 2439 private fun handleFullscreenTaskLaunch( 2440 task: RunningTaskInfo, 2441 transition: IBinder, 2442 ): WindowContainerTransaction? { 2443 logV("handleFullscreenTaskLaunch") 2444 if (shouldFullscreenTaskLaunchSwitchToDesktop(task)) { 2445 logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId) 2446 return WindowContainerTransaction().also { wct -> 2447 val deskId = getDefaultDeskId(task.displayId) 2448 addMoveToDeskTaskChanges(wct = wct, task = task, deskId = deskId) 2449 val runOnTransitStart: RunOnTransitStart? = 2450 if ( 2451 task.baseIntent.flags.and(Intent.FLAG_ACTIVITY_TASK_ON_HOME) != 0 || 2452 !isDesktopModeShowing(task.displayId) 2453 ) { 2454 // In some launches home task is moved behind new task being launched. Make 2455 // sure that's not the case for launches in desktop. Also, if this launch is 2456 // the first one to trigger the desktop mode (e.g., when 2457 // [forceEnterDesktop()]), activate the desk here. 2458 val activationRunnable = 2459 addDeskActivationChanges( 2460 deskId = deskId, 2461 wct = wct, 2462 newTask = task, 2463 addPendingLaunchTransition = true, 2464 ) 2465 wct.reorder(task.token, true) 2466 activationRunnable 2467 } else { 2468 { transition: IBinder -> 2469 // The desk was already showing and we're launching a new Task - we 2470 // might need to minimize another Task. 2471 val taskIdToMinimize = 2472 addAndGetMinimizeChanges(deskId, wct, task.taskId) 2473 taskIdToMinimize?.let { minimizingTaskId -> 2474 addPendingMinimizeTransition( 2475 transition, 2476 minimizingTaskId, 2477 MinimizeReason.TASK_LIMIT, 2478 ) 2479 } 2480 // Also track the pending launching task. 2481 addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) 2482 } 2483 } 2484 runOnTransitStart?.invoke(transition) 2485 desktopImmersiveController.exitImmersiveIfApplicable( 2486 transition, 2487 wct, 2488 task.displayId, 2489 reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, 2490 ) 2491 } 2492 } else if (taskRepository.isActiveTask(task.taskId)) { 2493 // If a freeform task receives a request for a fullscreen launch, apply the same 2494 // changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED 2495 // set when needed can interfere with future split / multi-instance transitions. 2496 val wct = WindowContainerTransaction() 2497 val runOnTransitStart = 2498 addMoveToFullscreenChanges( 2499 wct = wct, 2500 taskInfo = task, 2501 willExitDesktop = 2502 willExitDesktop( 2503 triggerTaskId = task.taskId, 2504 displayId = task.displayId, 2505 forceExitDesktop = true, 2506 ), 2507 ) 2508 runOnTransitStart?.invoke(transition) 2509 return wct 2510 } 2511 return null 2512 } 2513 2514 private fun shouldFreeformTaskLaunchSwitchToFullscreen(task: RunningTaskInfo): Boolean = 2515 !isDesktopModeShowing(task.displayId) 2516 2517 private fun shouldFullscreenTaskLaunchSwitchToDesktop(task: RunningTaskInfo): Boolean = 2518 isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId) 2519 2520 /** 2521 * If a task is not compatible with desktop mode freeform, it should always be launched in 2522 * fullscreen. 2523 */ 2524 private fun handleIncompatibleTaskLaunch( 2525 task: RunningTaskInfo, 2526 transition: IBinder, 2527 ): WindowContainerTransaction? { 2528 logV("handleIncompatibleTaskLaunch") 2529 if (!isDesktopModeShowing(task.displayId) && !forceEnterDesktop(task.displayId)) return null 2530 // Only update task repository for transparent task. 2531 if ( 2532 DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC 2533 .isTrue() && desktopModeCompatPolicy.isTransparentTask(task) 2534 ) { 2535 taskRepository.setTopTransparentFullscreenTaskId(task.displayId, task.taskId) 2536 } 2537 // Already fullscreen, no-op. 2538 if (task.isFullscreen) return null 2539 val wct = WindowContainerTransaction() 2540 val runOnTransitStart = 2541 addMoveToFullscreenChanges( 2542 wct = wct, 2543 taskInfo = task, 2544 willExitDesktop = 2545 willExitDesktop( 2546 triggerTaskId = task.taskId, 2547 displayId = task.displayId, 2548 forceExitDesktop = true, 2549 ), 2550 ) 2551 runOnTransitStart?.invoke(transition) 2552 return wct 2553 } 2554 2555 /** 2556 * Handle task closing by removing wallpaper activity if it's the last active task. 2557 * 2558 * TODO: b/394268248 - desk needs to be deactivated. 2559 */ 2560 private fun handleTaskClosing( 2561 task: RunningTaskInfo, 2562 transition: IBinder, 2563 requestType: Int, 2564 ): WindowContainerTransaction? { 2565 logV("handleTaskClosing") 2566 if (!isDesktopModeShowing(task.displayId)) return null 2567 val deskId = taskRepository.getDeskIdForTask(task.taskId) 2568 if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 2569 return null 2570 } 2571 2572 val wct = WindowContainerTransaction() 2573 val deactivationRunnable = 2574 performDesktopExitCleanupIfNeeded( 2575 taskId = task.taskId, 2576 deskId = deskId, 2577 displayId = task.displayId, 2578 wct = wct, 2579 forceToFullscreen = false, 2580 ) 2581 deactivationRunnable?.invoke(transition) 2582 2583 if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { 2584 taskRepository.addClosingTask( 2585 displayId = task.displayId, 2586 deskId = deskId, 2587 taskId = task.taskId, 2588 ) 2589 snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) 2590 } 2591 2592 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( 2593 doesAnyTaskRequireTaskbarRounding(task.displayId, task.taskId) 2594 ) 2595 return if (wct.isEmpty) null else wct 2596 } 2597 2598 /** 2599 * Applies the [wct] changes need when a task is first moving to a desk and the desk needs to be 2600 * activated. 2601 */ 2602 private fun addDeskActivationWithMovingTaskChanges( 2603 deskId: Int, 2604 wct: WindowContainerTransaction, 2605 task: RunningTaskInfo, 2606 ): RunOnTransitStart? { 2607 val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) 2608 addMoveToDeskTaskChanges(wct = wct, task = task, deskId = deskId) 2609 return runOnTransitStart 2610 } 2611 2612 /** 2613 * Applies the [wct] changes needed when a task is first moving to a desk. 2614 * 2615 * Note that this recalculates the initial bounds of the task, so it should not be used when 2616 * transferring a task between desks. 2617 * 2618 * TODO: b/362720497 - this should be improved to be reusable by desk-to-desk CUJs where 2619 * [DesksOrganizer.moveTaskToDesk] needs to be called and even cross-display CUJs where 2620 * [applyFreeformDisplayChange] needs to be called. Potentially by comparing source vs 2621 * destination desk ids and display ids, or adding extra arguments to the function. 2622 */ 2623 fun addMoveToDeskTaskChanges( 2624 wct: WindowContainerTransaction, 2625 task: RunningTaskInfo, 2626 deskId: Int, 2627 ) { 2628 val targetDisplayId = taskRepository.getDisplayForDesk(deskId) 2629 val displayLayout = displayController.getDisplayLayout(targetDisplayId) ?: return 2630 val inheritedTaskBounds = 2631 getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId) 2632 if (inheritedTaskBounds != null) { 2633 // Inherit bounds from closing task instance to prevent application jumping different 2634 // cascading positions. 2635 wct.setBounds(task.token, inheritedTaskBounds) 2636 } else { 2637 val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId) 2638 if (canChangeTaskPosition(task)) { 2639 wct.setBounds(task.token, initialBounds) 2640 } 2641 } 2642 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 2643 desksOrganizer.moveTaskToDesk(wct = wct, deskId = deskId, task = task) 2644 } else { 2645 val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(targetDisplayId)!! 2646 val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode 2647 val targetWindowingMode = 2648 if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) { 2649 // Display windowing is freeform, set to undefined and inherit it 2650 WINDOWING_MODE_UNDEFINED 2651 } else { 2652 WINDOWING_MODE_FREEFORM 2653 } 2654 wct.setWindowingMode(task.token, targetWindowingMode) 2655 wct.reorder(task.token, /* onTop= */ true) 2656 } 2657 if (useDesktopOverrideDensity()) { 2658 wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) 2659 } 2660 } 2661 2662 /** 2663 * Apply changes to move a freeform task from one display to another, which includes handling 2664 * density changes between displays. 2665 */ 2666 private fun applyFreeformDisplayChange( 2667 wct: WindowContainerTransaction, 2668 taskInfo: RunningTaskInfo, 2669 destDisplayId: Int, 2670 ) { 2671 val sourceLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return 2672 val destLayout = displayController.getDisplayLayout(destDisplayId) ?: return 2673 val bounds = taskInfo.configuration.windowConfiguration.bounds 2674 val scaledWidth = bounds.width() * destLayout.densityDpi() / sourceLayout.densityDpi() 2675 val scaledHeight = bounds.height() * destLayout.densityDpi() / sourceLayout.densityDpi() 2676 val sourceWidthMargin = sourceLayout.width() - bounds.width() 2677 val sourceHeightMargin = sourceLayout.height() - bounds.height() 2678 val destWidthMargin = destLayout.width() - scaledWidth 2679 val destHeightMargin = destLayout.height() - scaledHeight 2680 val scaledLeft = 2681 if (sourceWidthMargin != 0) { 2682 bounds.left * destWidthMargin / sourceWidthMargin 2683 } else { 2684 destWidthMargin / 2 2685 } 2686 val scaledTop = 2687 if (sourceHeightMargin != 0) { 2688 bounds.top * destHeightMargin / sourceHeightMargin 2689 } else { 2690 destHeightMargin / 2 2691 } 2692 val boundsWithinDisplay = 2693 if (destWidthMargin >= 0 && destHeightMargin >= 0) { 2694 Rect(0, 0, scaledWidth, scaledHeight).apply { 2695 offsetTo( 2696 scaledLeft.coerceIn(0, destWidthMargin), 2697 scaledTop.coerceIn(0, destHeightMargin), 2698 ) 2699 } 2700 } else { 2701 getInitialBounds(destLayout, taskInfo, destDisplayId) 2702 } 2703 wct.setBounds(taskInfo.token, boundsWithinDisplay) 2704 } 2705 2706 private fun getInitialBounds( 2707 displayLayout: DisplayLayout, 2708 taskInfo: RunningTaskInfo, 2709 displayId: Int, 2710 ): Rect { 2711 val bounds = 2712 if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue) { 2713 // If caption insets should be excluded from app bounds, ensure caption insets 2714 // are excluded from the ideal initial bounds when scaling non-resizeable apps. 2715 // Caption insets stay fixed and don't scale with bounds. 2716 val captionInsets = 2717 if (desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo)) { 2718 getDesktopViewAppHeaderHeightPx(context) 2719 } else { 2720 0 2721 } 2722 calculateInitialBounds(displayLayout, taskInfo, captionInsets = captionInsets) 2723 } else { 2724 calculateDefaultDesktopTaskBounds(displayLayout) 2725 } 2726 2727 if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue) { 2728 cascadeWindow(bounds, displayLayout, displayId) 2729 } 2730 return bounds 2731 } 2732 2733 /** 2734 * Applies the changes needed to enter fullscreen and returns the id of the desk that needs to 2735 * be deactivated. 2736 */ 2737 private fun addMoveToFullscreenChanges( 2738 wct: WindowContainerTransaction, 2739 taskInfo: RunningTaskInfo, 2740 willExitDesktop: Boolean, 2741 ): RunOnTransitStart? { 2742 val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! 2743 val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode 2744 val targetWindowingMode = 2745 if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) { 2746 // Display windowing is fullscreen, set to undefined and inherit it 2747 WINDOWING_MODE_UNDEFINED 2748 } else { 2749 WINDOWING_MODE_FULLSCREEN 2750 } 2751 wct.setWindowingMode(taskInfo.token, targetWindowingMode) 2752 wct.setBounds(taskInfo.token, Rect()) 2753 if (useDesktopOverrideDensity()) { 2754 wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) 2755 } 2756 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 2757 wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true) 2758 } 2759 val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) 2760 return performDesktopExitCleanUp( 2761 wct = wct, 2762 deskId = deskId, 2763 displayId = taskInfo.displayId, 2764 willExitDesktop = willExitDesktop, 2765 shouldEndUpAtHome = false, 2766 ) 2767 } 2768 2769 private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) { 2770 val stableBounds = Rect() 2771 displayLayout.getStableBoundsForDesktopMode(stableBounds) 2772 2773 val activeTasks = taskRepository.getExpandedTasksOrdered(displayId) 2774 activeTasks.firstOrNull()?.let { activeTask -> 2775 shellTaskOrganizer.getRunningTaskInfo(activeTask)?.let { 2776 cascadeWindow( 2777 context.resources, 2778 stableBounds, 2779 it.configuration.windowConfiguration.bounds, 2780 bounds, 2781 ) 2782 } 2783 } 2784 } 2785 2786 /** 2787 * Adds split screen changes to a transaction. Note that bounds are not reset here due to 2788 * animation; see {@link onDesktopSplitSelectAnimComplete} 2789 */ 2790 private fun addMoveToSplitChanges( 2791 wct: WindowContainerTransaction, 2792 taskInfo: RunningTaskInfo, 2793 deskId: Int?, 2794 ): RunOnTransitStart? { 2795 if (!DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue) { 2796 // This windowing mode is to get the transition animation started; once we complete 2797 // split select, we will change windowing mode to undefined and inherit from split 2798 // stage. 2799 // Going to undefined here causes task to flicker to the top left. 2800 // Cancelling the split select flow will revert it to fullscreen. 2801 wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) 2802 } 2803 // The task's density may have been overridden in freeform; revert it here as we don't 2804 // want it overridden in multi-window. 2805 wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) 2806 2807 return performDesktopExitCleanupIfNeeded( 2808 taskId = taskInfo.taskId, 2809 displayId = taskInfo.displayId, 2810 deskId = deskId, 2811 wct = wct, 2812 forceToFullscreen = true, 2813 shouldEndUpAtHome = false, 2814 ) 2815 } 2816 2817 /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ 2818 private fun addAndGetMinimizeChanges( 2819 deskId: Int, 2820 wct: WindowContainerTransaction, 2821 newTaskId: Int?, 2822 launchingNewIntent: Boolean = false, 2823 ): Int? { 2824 if (!desktopTasksLimiter.isPresent) return null 2825 require(newTaskId == null || !launchingNewIntent) 2826 return desktopTasksLimiter 2827 .get() 2828 .addAndGetMinimizeTaskChanges(deskId, wct, newTaskId, launchingNewIntent) 2829 } 2830 2831 private fun addPendingMinimizeTransition( 2832 transition: IBinder, 2833 taskIdToMinimize: Int, 2834 minimizeReason: MinimizeReason, 2835 ) { 2836 val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) 2837 desktopTasksLimiter.ifPresent { 2838 it.addPendingMinimizeChange( 2839 transition = transition, 2840 displayId = taskToMinimize?.displayId ?: DEFAULT_DISPLAY, 2841 taskId = taskIdToMinimize, 2842 minimizeReason = minimizeReason, 2843 ) 2844 } 2845 } 2846 2847 private fun addPendingUnminimizeTransition( 2848 transition: IBinder, 2849 displayId: Int, 2850 taskIdToUnminimize: Int, 2851 unminimizeReason: UnminimizeReason, 2852 ) { 2853 desktopTasksLimiter.ifPresent { 2854 it.addPendingUnminimizeChange( 2855 transition, 2856 displayId = displayId, 2857 taskId = taskIdToUnminimize, 2858 unminimizeReason, 2859 ) 2860 } 2861 } 2862 2863 private fun addPendingAppLaunchTransition( 2864 transition: IBinder, 2865 launchTaskId: Int, 2866 minimizeTaskId: Int?, 2867 ) { 2868 if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue) { 2869 return 2870 } 2871 // TODO b/359523924: pass immersive task here? 2872 desktopMixedTransitionHandler.addPendingMixedTransition( 2873 DesktopMixedTransitionHandler.PendingMixedTransition.Launch( 2874 transition, 2875 launchTaskId, 2876 minimizeTaskId, 2877 /* exitingImmersiveTask= */ null, 2878 ) 2879 ) 2880 } 2881 2882 private fun activateDefaultDeskInDisplay( 2883 displayId: Int, 2884 remoteTransition: RemoteTransition? = null, 2885 ) { 2886 val deskId = getDefaultDeskId(displayId) 2887 activateDesk(deskId, remoteTransition) 2888 } 2889 2890 /** 2891 * Applies the necessary [wct] changes to activate the given desk. 2892 * 2893 * When a task is being brought into a desk together with the activation, then [newTask] is not 2894 * null and may be used to run other desktop policies, such as minimizing another task if the 2895 * task limit has been exceeded. 2896 */ 2897 private fun addDeskActivationChanges( 2898 deskId: Int, 2899 wct: WindowContainerTransaction, 2900 newTask: TaskInfo? = null, 2901 // TODO: b/362720497 - should this be true in other places? Can it be calculated locally 2902 // without having to specify the value? 2903 addPendingLaunchTransition: Boolean = false, 2904 ): RunOnTransitStart? { 2905 logV("addDeskActivationChanges newTaskId=%d deskId=%d", newTask?.taskId, deskId) 2906 val newTaskIdInFront = newTask?.taskId 2907 val displayId = taskRepository.getDisplayForDesk(deskId) 2908 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 2909 val taskIdToMinimize = bringDesktopAppsToFront(displayId, wct, newTask?.taskId) 2910 return { transition -> 2911 taskIdToMinimize?.let { minimizingTaskId -> 2912 addPendingMinimizeTransition( 2913 transition = transition, 2914 taskIdToMinimize = minimizingTaskId, 2915 minimizeReason = MinimizeReason.TASK_LIMIT, 2916 ) 2917 } 2918 if (newTask != null && addPendingLaunchTransition) { 2919 addPendingAppLaunchTransition(transition, newTask.taskId, taskIdToMinimize) 2920 } 2921 } 2922 } 2923 prepareForDeskActivation(displayId, wct) 2924 desksOrganizer.activateDesk(wct, deskId) 2925 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( 2926 doesAnyTaskRequireTaskbarRounding(displayId) 2927 ) 2928 val expandedTasksOrderedFrontToBack = 2929 taskRepository.getExpandedTasksIdsInDeskOrdered(deskId = deskId) 2930 // If we're adding a new Task we might need to minimize an old one 2931 val taskIdToMinimize = 2932 desktopTasksLimiter 2933 .getOrNull() 2934 ?.getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront) 2935 if (taskIdToMinimize != null) { 2936 val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) 2937 // TODO(b/365725441): Handle non running task minimization 2938 if (taskToMinimize != null) { 2939 desksOrganizer.minimizeTask(wct, deskId, taskToMinimize) 2940 } 2941 } 2942 if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { 2943 expandedTasksOrderedFrontToBack 2944 .filter { taskId -> taskId != taskIdToMinimize } 2945 .reversed() 2946 .forEach { taskId -> 2947 val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) 2948 if (runningTaskInfo == null) { 2949 wct.startTask(taskId, createActivityOptionsForStartTask().toBundle()) 2950 } else { 2951 desksOrganizer.reorderTaskToFront(wct, deskId, runningTaskInfo) 2952 } 2953 } 2954 } 2955 val deactivatingDesk = taskRepository.getActiveDeskId(displayId)?.takeIf { it != deskId } 2956 val deactivationRunnable = prepareDeskDeactivationIfNeeded(wct, deactivatingDesk) 2957 return { transition -> 2958 val activateDeskTransition = 2959 if (newTaskIdInFront != null) { 2960 DeskTransition.ActiveDeskWithTask( 2961 token = transition, 2962 displayId = displayId, 2963 deskId = deskId, 2964 enterTaskId = newTaskIdInFront, 2965 ) 2966 } else { 2967 DeskTransition.ActivateDesk( 2968 token = transition, 2969 displayId = displayId, 2970 deskId = deskId, 2971 ) 2972 } 2973 desksTransitionObserver.addPendingTransition(activateDeskTransition) 2974 taskIdToMinimize?.let { minimizingTask -> 2975 addPendingMinimizeTransition(transition, minimizingTask, MinimizeReason.TASK_LIMIT) 2976 } 2977 deactivationRunnable?.invoke(transition) 2978 } 2979 } 2980 2981 /** Activates the given desk. */ 2982 fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) { 2983 val wct = WindowContainerTransaction() 2984 val runOnTransitStart = addDeskActivationChanges(deskId, wct) 2985 2986 val transitionType = transitionType(remoteTransition) 2987 val handler = 2988 remoteTransition?.let { 2989 OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) 2990 } 2991 2992 val transition = transitions.startTransition(transitionType, wct, handler) 2993 handler?.setTransition(transition) 2994 runOnTransitStart?.invoke(transition) 2995 2996 desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( 2997 FREEFORM_ANIMATION_DURATION 2998 ) 2999 } 3000 3001 /** 3002 * TODO: b/393978539 - Deactivation should not happen in desktop-first devices when going home. 3003 */ 3004 private fun prepareDeskDeactivationIfNeeded( 3005 wct: WindowContainerTransaction, 3006 deskId: Int?, 3007 ): RunOnTransitStart? { 3008 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return null 3009 if (deskId == null) return null 3010 desksOrganizer.deactivateDesk(wct, deskId) 3011 return { transition -> 3012 desksTransitionObserver.addPendingTransition( 3013 DeskTransition.DeactivateDesk(token = transition, deskId = deskId) 3014 ) 3015 } 3016 } 3017 3018 /** Removes the default desk in the given display. */ 3019 @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()")) 3020 fun removeDefaultDeskInDisplay(displayId: Int) { 3021 val deskId = getDefaultDeskId(displayId) 3022 removeDesk(displayId = displayId, deskId = deskId) 3023 } 3024 3025 private fun getDefaultDeskId(displayId: Int) = 3026 checkNotNull(taskRepository.getDefaultDeskId(displayId)) { 3027 "Expected a default desk to exist in display: $displayId" 3028 } 3029 3030 /** Removes the given desk. */ 3031 fun removeDesk(deskId: Int) { 3032 val displayId = taskRepository.getDisplayForDesk(deskId) 3033 removeDesk(displayId = displayId, deskId = deskId) 3034 } 3035 3036 /** Removes all the available desks on all displays. */ 3037 fun removeAllDesks() { 3038 taskRepository.getAllDeskIds().forEach { deskId -> removeDesk(deskId) } 3039 } 3040 3041 private fun removeDesk(displayId: Int, deskId: Int) { 3042 if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return 3043 logV("removeDesk deskId=%d from displayId=%d", deskId, displayId) 3044 3045 val tasksToRemove = 3046 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 3047 taskRepository.getActiveTaskIdsInDesk(deskId) 3048 } else { 3049 // TODO: 362720497 - make sure minimized windows are also removed in WM 3050 // and the repository. 3051 taskRepository.removeDesk(deskId) 3052 } 3053 3054 val wct = WindowContainerTransaction() 3055 tasksToRemove.forEach { 3056 // TODO: b/404595635 - consider moving this block into [DesksOrganizer]. 3057 val task = shellTaskOrganizer.getRunningTaskInfo(it) 3058 if (task != null) { 3059 wct.removeTask(task.token) 3060 } else { 3061 recentTasksController?.removeBackgroundTask(it) 3062 } 3063 } 3064 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 3065 desksOrganizer.removeDesk(wct, deskId, userId) 3066 } 3067 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return 3068 val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) 3069 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 3070 desksTransitionObserver.addPendingTransition( 3071 DeskTransition.RemoveDesk( 3072 token = transition, 3073 displayId = displayId, 3074 deskId = deskId, 3075 tasks = tasksToRemove, 3076 onDeskRemovedListener = onDeskRemovedListener, 3077 ) 3078 ) 3079 } 3080 } 3081 3082 /** Enter split by using the focused desktop task in given `displayId`. */ 3083 fun enterSplit(displayId: Int, leftOrTop: Boolean) { 3084 getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) } 3085 } 3086 3087 private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? = 3088 shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> 3089 taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM 3090 } 3091 3092 /** 3093 * Requests a task be transitioned from desktop to split select. Applies needed windowing 3094 * changes if this transition is enabled. 3095 */ 3096 @JvmOverloads 3097 fun requestSplit(taskInfo: RunningTaskInfo, leftOrTop: Boolean = false) { 3098 // If a drag to desktop is in progress, we want to enter split select 3099 // even if the requesting task is already in split. 3100 val isDragging = dragToDesktopTransitionHandler.inProgress 3101 val shouldRequestSplit = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging 3102 if (shouldRequestSplit) { 3103 if (isDragging) { 3104 releaseVisualIndicator() 3105 val cancelState = 3106 if (leftOrTop) { 3107 DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT 3108 } else { 3109 DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT 3110 } 3111 dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState) 3112 } else { 3113 val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) 3114 logV("Split requested for task=%d in desk=%d", taskInfo.taskId, deskId) 3115 val wct = WindowContainerTransaction() 3116 val runOnTransitStart = addMoveToSplitChanges(wct, taskInfo, deskId) 3117 val transition = 3118 splitScreenController.requestEnterSplitSelect( 3119 taskInfo, 3120 wct, 3121 if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT 3122 else SPLIT_POSITION_BOTTOM_OR_RIGHT, 3123 taskInfo.configuration.windowConfiguration.bounds, 3124 ) 3125 if (transition != null) { 3126 runOnTransitStart?.invoke(transition) 3127 } 3128 } 3129 } 3130 } 3131 3132 /** Requests a task be transitioned from whatever mode it's in to a bubble. */ 3133 @JvmOverloads 3134 fun requestFloat(taskInfo: RunningTaskInfo, left: Boolean? = null) { 3135 val isDragging = dragToDesktopTransitionHandler.inProgress 3136 val shouldRequestFloat = 3137 taskInfo.isFullscreen || taskInfo.isFreeform || isDragging || taskInfo.isMultiWindow 3138 if (!shouldRequestFloat) return 3139 if (isDragging) { 3140 releaseVisualIndicator() 3141 val cancelState = 3142 if (left == true) DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_LEFT 3143 else DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_RIGHT 3144 dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState) 3145 } else { 3146 bubbleController.ifPresent { 3147 it.expandStackAndSelectBubble(taskInfo, /* dragData= */ null) 3148 } 3149 } 3150 } 3151 3152 private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi 3153 3154 /** Creates a new instance of the external interface to pass to another process. */ 3155 private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this) 3156 3157 /** Get connection interface between sysui and shell */ 3158 fun asDesktopMode(): DesktopMode { 3159 return desktopMode 3160 } 3161 3162 /** 3163 * Perform checks required on drag move. Create/release fullscreen indicator as needed. 3164 * Different sources for x and y coordinates are used due to different needs for each: We want 3165 * split transitions to be based on input coordinates but fullscreen transition to be based on 3166 * task edge coordinate. 3167 * 3168 * @param taskInfo the task being dragged. 3169 * @param taskSurface SurfaceControl of dragged task. 3170 * @param inputX x coordinate of input. Used for checks against left/right edge of screen. 3171 * @param taskBounds bounds of dragged task. Used for checks against status bar height. 3172 */ 3173 fun onDragPositioningMove( 3174 taskInfo: RunningTaskInfo, 3175 taskSurface: SurfaceControl, 3176 inputX: Float, 3177 taskBounds: Rect, 3178 ) { 3179 if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return 3180 snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) 3181 updateVisualIndicator( 3182 taskInfo, 3183 taskSurface, 3184 inputX, 3185 taskBounds.top.toFloat(), 3186 DragStartState.FROM_FREEFORM, 3187 ) 3188 } 3189 3190 fun updateVisualIndicator( 3191 taskInfo: RunningTaskInfo, 3192 taskSurface: SurfaceControl?, 3193 inputX: Float, 3194 taskTop: Float, 3195 dragStartState: DragStartState, 3196 ): DesktopModeVisualIndicator.IndicatorType { 3197 // If the visual indicator has the wrong start state, it was never cleared from a previous 3198 // drag event and needs to be cleared 3199 if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) { 3200 Slog.e(TAG, "Visual indicator from previous motion event was never released") 3201 releaseVisualIndicator() 3202 } 3203 // If the visual indicator does not exist, create it. 3204 val indicator = 3205 visualIndicator 3206 ?: DesktopModeVisualIndicator( 3207 desktopExecutor, 3208 mainExecutor, 3209 syncQueue, 3210 taskInfo, 3211 displayController, 3212 if (Flags.enableBugFixesForSecondaryDisplay()) { 3213 displayController.getDisplayContext(taskInfo.displayId) 3214 } else { 3215 context 3216 }, 3217 taskSurface, 3218 rootTaskDisplayAreaOrganizer, 3219 dragStartState, 3220 bubbleController.getOrNull()?.bubbleDropTargetBoundsProvider, 3221 snapEventHandler, 3222 ) 3223 if (visualIndicator == null) visualIndicator = indicator 3224 return indicator.updateIndicatorType(PointF(inputX, taskTop)) 3225 } 3226 3227 /** 3228 * Perform checks required on drag end. If indicator indicates a windowing mode change, perform 3229 * that change. Otherwise, ensure bounds are up to date. 3230 * 3231 * @param taskInfo the task being dragged. 3232 * @param taskSurface the leash of the task being dragged. 3233 * @param inputCoordinate the coordinates of the motion event 3234 * @param currentDragBounds the current bounds of where the visible task is (might be actual 3235 * task bounds or just task leash) 3236 * @param validDragArea the bounds of where the task can be dragged within the display. 3237 * @param dragStartBounds the bounds of the task before starting dragging. 3238 */ 3239 fun onDragPositioningEnd( 3240 taskInfo: RunningTaskInfo, 3241 taskSurface: SurfaceControl, 3242 inputCoordinate: PointF, 3243 currentDragBounds: Rect, 3244 validDragArea: Rect, 3245 dragStartBounds: Rect, 3246 motionEvent: MotionEvent, 3247 ) { 3248 if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { 3249 return 3250 } 3251 3252 val indicator = getVisualIndicator() ?: return 3253 val indicatorType = 3254 indicator.updateIndicatorType( 3255 PointF(inputCoordinate.x, currentDragBounds.top.toFloat()) 3256 ) 3257 when (indicatorType) { 3258 IndicatorType.TO_FULLSCREEN_INDICATOR -> { 3259 if (DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)) { 3260 dragToMaximizeDesktopTask(taskInfo, taskSurface, currentDragBounds, motionEvent) 3261 } else { 3262 desktopModeUiEventLogger.log( 3263 taskInfo, 3264 DesktopUiEventEnum.DESKTOP_WINDOW_APP_HEADER_DRAG_TO_FULL_SCREEN, 3265 ) 3266 moveToFullscreenWithAnimation( 3267 taskInfo, 3268 Point(currentDragBounds.left, currentDragBounds.top), 3269 DesktopModeTransitionSource.TASK_DRAG, 3270 ) 3271 } 3272 } 3273 IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { 3274 desktopModeUiEventLogger.log( 3275 taskInfo, 3276 DesktopUiEventEnum.DESKTOP_WINDOW_APP_HEADER_DRAG_TO_TILE_TO_LEFT, 3277 ) 3278 handleSnapResizingTaskOnDrag( 3279 taskInfo, 3280 SnapPosition.LEFT, 3281 taskSurface, 3282 currentDragBounds, 3283 dragStartBounds, 3284 motionEvent, 3285 ) 3286 } 3287 IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { 3288 desktopModeUiEventLogger.log( 3289 taskInfo, 3290 DesktopUiEventEnum.DESKTOP_WINDOW_APP_HEADER_DRAG_TO_TILE_TO_RIGHT, 3291 ) 3292 handleSnapResizingTaskOnDrag( 3293 taskInfo, 3294 SnapPosition.RIGHT, 3295 taskSurface, 3296 currentDragBounds, 3297 dragStartBounds, 3298 motionEvent, 3299 ) 3300 } 3301 IndicatorType.NO_INDICATOR, 3302 IndicatorType.TO_BUBBLE_LEFT_INDICATOR, 3303 IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> { 3304 // TODO(b/391928049): add support fof dragging desktop apps to a bubble 3305 3306 // Create a copy so that we can animate from the current bounds if we end up having 3307 // to snap the surface back without a WCT change. 3308 val destinationBounds = Rect(currentDragBounds) 3309 // If task bounds are outside valid drag area, snap them inward 3310 DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( 3311 destinationBounds, 3312 validDragArea, 3313 ) 3314 3315 if (destinationBounds == dragStartBounds) { 3316 // There's no actual difference between the start and end bounds, so while a 3317 // WCT change isn't needed, the dragged surface still needs to be snapped back 3318 // to its original location. 3319 releaseVisualIndicator() 3320 returnToDragStartAnimator.start( 3321 taskInfo.taskId, 3322 taskSurface, 3323 startBounds = currentDragBounds, 3324 endBounds = dragStartBounds, 3325 ) 3326 return 3327 } 3328 3329 // Update task bounds so that the task position will match the position of its leash 3330 val wct = WindowContainerTransaction() 3331 wct.setBounds(taskInfo.token, destinationBounds) 3332 3333 val newDisplayId = motionEvent.getDisplayId() 3334 val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId) 3335 val isCrossDisplayDrag = 3336 Flags.enableConnectedDisplaysWindowDrag() && 3337 newDisplayId != taskInfo.getDisplayId() && 3338 displayAreaInfo != null 3339 val handler = 3340 if (isCrossDisplayDrag) { 3341 dragToDisplayTransitionHandler 3342 } else { 3343 null 3344 } 3345 if (isCrossDisplayDrag) { 3346 // TODO: b/362720497 - reparent to a specific desk within the target display. 3347 wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true) 3348 } 3349 3350 transitions.startTransition(TRANSIT_CHANGE, wct, handler) 3351 3352 releaseVisualIndicator() 3353 } 3354 IndicatorType.TO_DESKTOP_INDICATOR -> { 3355 throw IllegalArgumentException( 3356 "Should not be receiving TO_DESKTOP_INDICATOR for " + "a freeform task." 3357 ) 3358 } 3359 } 3360 // A freeform drag-move ended, remove the indicator immediately. 3361 releaseVisualIndicator() 3362 taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( 3363 doesAnyTaskRequireTaskbarRounding(taskInfo.displayId) 3364 ) 3365 } 3366 3367 /** 3368 * Cancel the drag-to-desktop transition. 3369 * 3370 * @param taskInfo the task being dragged. 3371 */ 3372 fun onDragPositioningCancelThroughStatusBar(taskInfo: RunningTaskInfo) { 3373 interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) 3374 cancelDragToDesktop(taskInfo) 3375 } 3376 3377 /** 3378 * Perform checks required when drag ends under status bar area. 3379 * 3380 * @param taskInfo the task being dragged. 3381 * @param y height of drag, to be checked against status bar height. 3382 * @return the [IndicatorType] used for the resulting transition 3383 */ 3384 fun onDragPositioningEndThroughStatusBar( 3385 inputCoordinates: PointF, 3386 taskInfo: RunningTaskInfo, 3387 taskSurface: SurfaceControl, 3388 ): IndicatorType { 3389 // End the drag_hold CUJ interaction. 3390 interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) 3391 val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR 3392 val indicatorType = indicator.updateIndicatorType(inputCoordinates) 3393 when (indicatorType) { 3394 IndicatorType.TO_DESKTOP_INDICATOR -> { 3395 LatencyTracker.getInstance(context) 3396 .onActionStart(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) 3397 // Start a new jank interaction for the drag release to desktop window animation. 3398 interactionJankMonitor.begin( 3399 taskSurface, 3400 context, 3401 handler, 3402 CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, 3403 "to_desktop", 3404 ) 3405 desktopModeUiEventLogger.log( 3406 taskInfo, 3407 DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_DESKTOP_MODE, 3408 ) 3409 finalizeDragToDesktop(taskInfo) 3410 } 3411 IndicatorType.NO_INDICATOR, 3412 IndicatorType.TO_FULLSCREEN_INDICATOR -> { 3413 desktopModeUiEventLogger.log( 3414 taskInfo, 3415 DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_FULL_SCREEN, 3416 ) 3417 cancelDragToDesktop(taskInfo) 3418 } 3419 IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { 3420 desktopModeUiEventLogger.log( 3421 taskInfo, 3422 DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_SPLIT_SCREEN, 3423 ) 3424 requestSplit(taskInfo, leftOrTop = true) 3425 } 3426 IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { 3427 desktopModeUiEventLogger.log( 3428 taskInfo, 3429 DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_SPLIT_SCREEN, 3430 ) 3431 requestSplit(taskInfo, leftOrTop = false) 3432 } 3433 IndicatorType.TO_BUBBLE_LEFT_INDICATOR -> { 3434 requestFloat(taskInfo, left = true) 3435 } 3436 IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> { 3437 requestFloat(taskInfo, left = false) 3438 } 3439 } 3440 return indicatorType 3441 } 3442 3443 /** Update the exclusion region for a specified task */ 3444 fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) { 3445 taskRepository.updateTaskExclusionRegions(taskId, exclusionRegion) 3446 } 3447 3448 /** Remove a previously tracked exclusion region for a specified task. */ 3449 fun removeExclusionRegionForTask(taskId: Int) { 3450 taskRepository.removeExclusionRegion(taskId) 3451 } 3452 3453 /** 3454 * Adds a listener to find out about changes in the visibility of freeform tasks. 3455 * 3456 * @param listener the listener to add. 3457 * @param callbackExecutor the executor to call the listener on. 3458 */ 3459 fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) { 3460 taskRepository.addVisibleTasksListener(listener, callbackExecutor) 3461 } 3462 3463 /** 3464 * Adds a listener to track changes to desktop task gesture exclusion regions 3465 * 3466 * @param listener the listener to add. 3467 * @param callbackExecutor the executor to call the listener on. 3468 */ 3469 fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) { 3470 taskRepository.setExclusionRegionListener(listener, callbackExecutor) 3471 } 3472 3473 // TODO(b/358114479): Move this implementation into a separate class. 3474 override fun onUnhandledDrag( 3475 launchIntent: PendingIntent, 3476 @UserIdInt userId: Int, 3477 dragEvent: DragEvent, 3478 onFinishCallback: Consumer<Boolean>, 3479 ): Boolean { 3480 // TODO(b/320797628): Pass through which display we are dropping onto 3481 if (!isDesktopModeShowing(DEFAULT_DISPLAY)) { 3482 // Not currently in desktop mode, ignore the drop 3483 return false 3484 } 3485 3486 // TODO: 3487 val launchComponent = getComponent(launchIntent) 3488 if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent, userId)) { 3489 // TODO(b/320797628): Should only return early if there is an existing running task, and 3490 // notify the user as well. But for now, just ignore the drop. 3491 logV("Dropped intent does not support multi-instance") 3492 return false 3493 } 3494 val taskInfo = getFocusedFreeformTask(DEFAULT_DISPLAY) ?: return false 3495 // TODO(b/358114479): Update drag and drop handling to give us visibility into when another 3496 // window will accept a drag event. This way, we can hide the indicator when we won't 3497 // be handling the transition here, allowing us to display the indicator accurately. 3498 // For now, we create the indicator only on drag end and immediately dispose it. 3499 val indicatorType = 3500 updateVisualIndicator( 3501 taskInfo, 3502 dragEvent.dragSurface, 3503 dragEvent.x, 3504 dragEvent.y, 3505 DragStartState.DRAGGED_INTENT, 3506 ) 3507 releaseVisualIndicator() 3508 val windowingMode = 3509 when (indicatorType) { 3510 IndicatorType.TO_FULLSCREEN_INDICATOR -> { 3511 WINDOWING_MODE_FULLSCREEN 3512 } 3513 IndicatorType.TO_SPLIT_LEFT_INDICATOR, 3514 IndicatorType.TO_SPLIT_RIGHT_INDICATOR, 3515 IndicatorType.TO_DESKTOP_INDICATOR -> { 3516 WINDOWING_MODE_FREEFORM 3517 } 3518 else -> error("Invalid indicator type: $indicatorType") 3519 } 3520 val displayLayout = displayController.getDisplayLayout(DEFAULT_DISPLAY) ?: return false 3521 val newWindowBounds = Rect() 3522 when (indicatorType) { 3523 IndicatorType.TO_DESKTOP_INDICATOR -> { 3524 // Use default bounds, but with the top-center at the drop point. 3525 newWindowBounds.set(calculateDefaultDesktopTaskBounds(displayLayout)) 3526 newWindowBounds.offsetTo( 3527 dragEvent.x.toInt() - (newWindowBounds.width() / 2), 3528 dragEvent.y.toInt(), 3529 ) 3530 } 3531 IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { 3532 newWindowBounds.set(getSnapBounds(taskInfo, SnapPosition.RIGHT)) 3533 } 3534 IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { 3535 newWindowBounds.set(getSnapBounds(taskInfo, SnapPosition.LEFT)) 3536 } 3537 else -> { 3538 // Use empty bounds for the fullscreen case. 3539 } 3540 } 3541 // Start a new transition to launch the app 3542 val opts = 3543 ActivityOptions.makeBasic().apply { 3544 launchWindowingMode = windowingMode 3545 launchBounds = newWindowBounds 3546 pendingIntentBackgroundActivityStartMode = 3547 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS 3548 pendingIntentLaunchFlags = 3549 Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK 3550 splashScreenStyle = SPLASH_SCREEN_STYLE_ICON 3551 } 3552 if (windowingMode == WINDOWING_MODE_FULLSCREEN) { 3553 dragAndDropFullscreenCookie = Binder() 3554 opts.launchCookie = dragAndDropFullscreenCookie 3555 } 3556 val wct = WindowContainerTransaction() 3557 wct.sendPendingIntent(launchIntent, null, opts.toBundle()) 3558 if (windowingMode == WINDOWING_MODE_FREEFORM) { 3559 if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) { 3560 // TODO b/376389593: Use a custom tab tearing transition/animation 3561 val deskId = getDefaultDeskId(DEFAULT_DISPLAY) 3562 startLaunchTransition( 3563 TRANSIT_OPEN, 3564 wct, 3565 launchingTaskId = null, 3566 deskId = deskId, 3567 displayId = DEFAULT_DISPLAY, 3568 ) 3569 } else { 3570 desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) 3571 } 3572 } else { 3573 transitions.startTransition(TRANSIT_OPEN, wct, null) 3574 } 3575 3576 // Report that this is handled by the listener 3577 onFinishCallback.accept(true) 3578 3579 // We've assumed responsibility of cleaning up the drag surface, so do that now 3580 // TODO(b/320797628): Do an actual animation here for the drag surface 3581 val t = SurfaceControl.Transaction() 3582 t.remove(dragEvent.dragSurface) 3583 t.apply() 3584 return true 3585 } 3586 3587 // TODO(b/366397912): Support full multi-user mode in Windowing. 3588 override fun onUserChanged(newUserId: Int, userContext: Context) { 3589 logV("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId) 3590 updateCurrentUser(newUserId) 3591 } 3592 3593 private fun updateCurrentUser(newUserId: Int) { 3594 userId = newUserId 3595 taskRepository = userRepositories.getProfile(userId) 3596 if (this::snapEventHandler.isInitialized) { 3597 snapEventHandler.onUserChange() 3598 } 3599 } 3600 3601 /** Called when a task's info changes. */ 3602 fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { 3603 if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return 3604 val inImmersive = taskRepository.isTaskInFullImmersiveState(taskInfo.taskId) 3605 val requestingImmersive = taskInfo.requestingImmersive 3606 if ( 3607 inImmersive && 3608 !requestingImmersive && 3609 !RecentsTransitionStateListener.isRunning(recentsTransitionState) 3610 ) { 3611 // Exit immersive if the app is no longer requesting it. 3612 desktopImmersiveController.moveTaskToNonImmersive( 3613 taskInfo, 3614 DesktopImmersiveController.ExitReason.APP_NOT_IMMERSIVE, 3615 ) 3616 } 3617 } 3618 3619 private fun createActivityOptionsForStartTask(): ActivityOptions { 3620 return ActivityOptions.makeBasic().apply { 3621 launchWindowingMode = WINDOWING_MODE_FREEFORM 3622 splashScreenStyle = SPLASH_SCREEN_STYLE_ICON 3623 } 3624 } 3625 3626 private fun dump(pw: PrintWriter, prefix: String) { 3627 val innerPrefix = "$prefix " 3628 pw.println("${prefix}DesktopTasksController") 3629 DesktopModeStatus.dump(pw, innerPrefix, context) 3630 userRepositories.dump(pw, innerPrefix) 3631 focusTransitionObserver.dump(pw, innerPrefix) 3632 } 3633 3634 /** The interface for calls from outside the shell, within the host process. */ 3635 @ExternalThread 3636 private inner class DesktopModeImpl : DesktopMode { 3637 override fun addVisibleTasksListener( 3638 listener: VisibleTasksListener, 3639 callbackExecutor: Executor, 3640 ) { 3641 mainExecutor.execute { 3642 this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor) 3643 } 3644 } 3645 3646 override fun addDesktopGestureExclusionRegionListener( 3647 listener: Consumer<Region>, 3648 callbackExecutor: Executor, 3649 ) { 3650 mainExecutor.execute { 3651 this@DesktopTasksController.setTaskRegionListener(listener, callbackExecutor) 3652 } 3653 } 3654 3655 override fun moveFocusedTaskToDesktop( 3656 displayId: Int, 3657 transitionSource: DesktopModeTransitionSource, 3658 ) { 3659 logV("moveFocusedTaskToDesktop") 3660 mainExecutor.execute { 3661 this@DesktopTasksController.moveFocusedTaskToDesktop(displayId, transitionSource) 3662 } 3663 } 3664 3665 override fun moveFocusedTaskToFullscreen( 3666 displayId: Int, 3667 transitionSource: DesktopModeTransitionSource, 3668 ) { 3669 logV("moveFocusedTaskToFullscreen") 3670 mainExecutor.execute { 3671 this@DesktopTasksController.enterFullscreen(displayId, transitionSource) 3672 } 3673 } 3674 3675 override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) { 3676 logV("moveFocusedTaskToStageSplit") 3677 mainExecutor.execute { this@DesktopTasksController.enterSplit(displayId, leftOrTop) } 3678 } 3679 } 3680 3681 /** The interface for calls from outside the host process. */ 3682 @BinderThread 3683 private class IDesktopModeImpl(private var controller: DesktopTasksController?) : 3684 IDesktopMode.Stub(), ExternalInterfaceBinder { 3685 3686 private lateinit var remoteListener: 3687 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener> 3688 3689 private val deskChangeListener: DeskChangeListener = 3690 object : DeskChangeListener { 3691 override fun onDeskAdded(displayId: Int, deskId: Int) { 3692 ProtoLog.v( 3693 WM_SHELL_DESKTOP_MODE, 3694 "IDesktopModeImpl: onDeskAdded display=%d deskId=%d", 3695 displayId, 3696 deskId, 3697 ) 3698 remoteListener.call { l -> l.onDeskAdded(displayId, deskId) } 3699 } 3700 3701 override fun onDeskRemoved(displayId: Int, deskId: Int) { 3702 ProtoLog.v( 3703 WM_SHELL_DESKTOP_MODE, 3704 "IDesktopModeImpl: onDeskRemoved display=%d deskId=%d", 3705 displayId, 3706 deskId, 3707 ) 3708 remoteListener.call { l -> l.onDeskRemoved(displayId, deskId) } 3709 } 3710 3711 override fun onActiveDeskChanged( 3712 displayId: Int, 3713 newActiveDeskId: Int, 3714 oldActiveDeskId: Int, 3715 ) { 3716 ProtoLog.v( 3717 WM_SHELL_DESKTOP_MODE, 3718 "IDesktopModeImpl: onActiveDeskChanged display=%d new=%d old=%d", 3719 displayId, 3720 newActiveDeskId, 3721 oldActiveDeskId, 3722 ) 3723 remoteListener.call { l -> 3724 l.onActiveDeskChanged(displayId, newActiveDeskId, oldActiveDeskId) 3725 } 3726 } 3727 } 3728 3729 private val visibleTasksListener: VisibleTasksListener = 3730 object : VisibleTasksListener { 3731 override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { 3732 ProtoLog.v( 3733 WM_SHELL_DESKTOP_MODE, 3734 "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d", 3735 displayId, 3736 visibleTasksCount, 3737 ) 3738 remoteListener.call { l -> 3739 l.onTasksVisibilityChanged(displayId, visibleTasksCount) 3740 } 3741 } 3742 } 3743 3744 private val taskbarDesktopTaskListener: TaskbarDesktopTaskListener = 3745 object : TaskbarDesktopTaskListener { 3746 override fun onTaskbarCornerRoundingUpdate( 3747 hasTasksRequiringTaskbarRounding: Boolean 3748 ) { 3749 ProtoLog.v( 3750 WM_SHELL_DESKTOP_MODE, 3751 "IDesktopModeImpl: onTaskbarCornerRoundingUpdate " + 3752 "doesAnyTaskRequireTaskbarRounding=%s", 3753 hasTasksRequiringTaskbarRounding, 3754 ) 3755 3756 remoteListener.call { l -> 3757 l.onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding) 3758 } 3759 } 3760 } 3761 3762 private val desktopModeEntryExitTransitionListener: DesktopModeEntryExitTransitionListener = 3763 object : DesktopModeEntryExitTransitionListener { 3764 override fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) { 3765 ProtoLog.v( 3766 WM_SHELL_DESKTOP_MODE, 3767 "IDesktopModeImpl: onEnterDesktopModeTransitionStarted transitionTime=%s", 3768 transitionDuration, 3769 ) 3770 remoteListener.call { l -> 3771 l.onEnterDesktopModeTransitionStarted(transitionDuration) 3772 } 3773 } 3774 3775 override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) { 3776 ProtoLog.v( 3777 WM_SHELL_DESKTOP_MODE, 3778 "IDesktopModeImpl: onExitDesktopModeTransitionStarted transitionTime=%s", 3779 transitionDuration, 3780 ) 3781 remoteListener.call { l -> 3782 l.onExitDesktopModeTransitionStarted(transitionDuration) 3783 } 3784 } 3785 } 3786 3787 init { 3788 remoteListener = 3789 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( 3790 controller, 3791 { c -> 3792 run { 3793 syncInitialState(c) 3794 registerListeners(c) 3795 } 3796 }, 3797 { c -> run { unregisterListeners(c) } }, 3798 ) 3799 } 3800 3801 /** Invalidates this instance, preventing future calls from updating the controller. */ 3802 override fun invalidate() { 3803 remoteListener.unregister() 3804 controller = null 3805 } 3806 3807 override fun createDesk(displayId: Int) { 3808 executeRemoteCallWithTaskPermission(controller, "createDesk") { c -> 3809 c.createDesk(displayId) 3810 } 3811 } 3812 3813 override fun removeDesk(deskId: Int) { 3814 executeRemoteCallWithTaskPermission(controller, "removeDesk") { c -> 3815 c.removeDesk(deskId) 3816 } 3817 } 3818 3819 override fun removeAllDesks() { 3820 executeRemoteCallWithTaskPermission(controller, "removeAllDesks") { c -> 3821 c.removeAllDesks() 3822 } 3823 } 3824 3825 override fun activateDesk(deskId: Int, remoteTransition: RemoteTransition?) { 3826 executeRemoteCallWithTaskPermission(controller, "activateDesk") { c -> 3827 c.activateDesk(deskId, remoteTransition) 3828 } 3829 } 3830 3831 override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) { 3832 executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c -> 3833 c.showDesktopApps(displayId, remoteTransition) 3834 } 3835 } 3836 3837 override fun showDesktopApp( 3838 taskId: Int, 3839 remoteTransition: RemoteTransition?, 3840 toFrontReason: DesktopTaskToFrontReason, 3841 ) { 3842 executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c -> 3843 c.moveTaskToFront(taskId, remoteTransition, toFrontReason.toUnminimizeReason()) 3844 } 3845 } 3846 3847 override fun stashDesktopApps(displayId: Int) { 3848 ProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: stashDesktopApps is deprecated") 3849 } 3850 3851 override fun hideStashedDesktopApps(displayId: Int) { 3852 ProtoLog.w( 3853 WM_SHELL_DESKTOP_MODE, 3854 "IDesktopModeImpl: hideStashedDesktopApps is deprecated", 3855 ) 3856 } 3857 3858 override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { 3859 executeRemoteCallWithTaskPermission(controller, "onDesktopSplitSelectAnimComplete") { c 3860 -> 3861 c.onDesktopSplitSelectAnimComplete(taskInfo) 3862 } 3863 } 3864 3865 override fun setTaskListener(listener: IDesktopTaskListener?) { 3866 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: set task listener=%s", listener) 3867 executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ -> 3868 listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() 3869 } 3870 } 3871 3872 override fun moveToDesktop( 3873 taskId: Int, 3874 transitionSource: DesktopModeTransitionSource, 3875 remoteTransition: RemoteTransition?, 3876 callback: IMoveToDesktopCallback?, 3877 ) { 3878 executeRemoteCallWithTaskPermission(controller, "moveTaskToDesktop") { c -> 3879 c.moveTaskToDefaultDeskAndActivate( 3880 taskId, 3881 transitionSource = transitionSource, 3882 remoteTransition = remoteTransition, 3883 callback = callback, 3884 ) 3885 } 3886 } 3887 3888 override fun removeDefaultDeskInDisplay(displayId: Int) { 3889 executeRemoteCallWithTaskPermission(controller, "removeDefaultDeskInDisplay") { c -> 3890 c.removeDefaultDeskInDisplay(displayId) 3891 } 3892 } 3893 3894 override fun moveToExternalDisplay(taskId: Int) { 3895 executeRemoteCallWithTaskPermission(controller, "moveTaskToExternalDisplay") { c -> 3896 c.moveToNextDisplay(taskId) 3897 } 3898 } 3899 3900 override fun startLaunchIntentTransition(intent: Intent, options: Bundle, displayId: Int) { 3901 executeRemoteCallWithTaskPermission(controller, "startLaunchIntentTransition") { c -> 3902 c.startLaunchIntentTransition(intent, options, displayId) 3903 } 3904 } 3905 3906 private fun syncInitialState(c: DesktopTasksController) { 3907 remoteListener.call { l -> 3908 // TODO: b/393962589 - implement desks limit. 3909 val canCreateDesks = true 3910 l.onListenerConnected( 3911 c.taskRepository.getDeskDisplayStateForRemote(), 3912 canCreateDesks, 3913 ) 3914 } 3915 } 3916 3917 private fun registerListeners(c: DesktopTasksController) { 3918 c.taskRepository.addDeskChangeListener(deskChangeListener, c.mainExecutor) 3919 c.taskRepository.addVisibleTasksListener(visibleTasksListener, c.mainExecutor) 3920 c.taskbarDesktopTaskListener = taskbarDesktopTaskListener 3921 c.desktopModeEnterExitTransitionListener = desktopModeEntryExitTransitionListener 3922 } 3923 3924 private fun unregisterListeners(c: DesktopTasksController) { 3925 c.taskRepository.removeDeskChangeListener(deskChangeListener) 3926 c.taskRepository.removeVisibleTasksListener(visibleTasksListener) 3927 c.taskbarDesktopTaskListener = null 3928 c.desktopModeEnterExitTransitionListener = null 3929 } 3930 } 3931 3932 private fun logV(msg: String, vararg arguments: Any?) { 3933 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 3934 } 3935 3936 private fun logD(msg: String, vararg arguments: Any?) { 3937 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 3938 } 3939 3940 private fun logW(msg: String, vararg arguments: Any?) { 3941 ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 3942 } 3943 3944 companion object { 3945 @JvmField 3946 val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = 3947 SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f 3948 3949 // Timeout used for CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD, this is longer than the 3950 // default timeout to avoid timing out in the middle of a drag action. 3951 private val APP_HANDLE_DRAG_HOLD_CUJ_TIMEOUT_MS: Long = TimeUnit.SECONDS.toMillis(10L) 3952 3953 private const val TAG = "DesktopTasksController" 3954 3955 private fun DesktopTaskToFrontReason.toUnminimizeReason(): UnminimizeReason = 3956 when (this) { 3957 DesktopTaskToFrontReason.UNKNOWN -> UnminimizeReason.UNKNOWN 3958 DesktopTaskToFrontReason.TASKBAR_TAP -> UnminimizeReason.TASKBAR_TAP 3959 DesktopTaskToFrontReason.ALT_TAB -> UnminimizeReason.ALT_TAB 3960 DesktopTaskToFrontReason.TASKBAR_MANAGE_WINDOW -> 3961 UnminimizeReason.TASKBAR_MANAGE_WINDOW 3962 } 3963 3964 @JvmField 3965 /** 3966 * A placeholder for a synthetic transition that isn't backed by a true system transition. 3967 */ 3968 val SYNTHETIC_TRANSITION: IBinder = Binder() 3969 } 3970 3971 /** Defines interface for classes that can listen to changes for task resize. */ 3972 // TODO(b/343931111): Migrate to using TransitionObservers when ready 3973 interface TaskbarDesktopTaskListener { 3974 /** 3975 * [hasTasksRequiringTaskbarRounding] is true when a task is either maximized or snapped 3976 * left/right and rounded corners are enabled. 3977 */ 3978 fun onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding: Boolean) 3979 } 3980 3981 /** Defines interface for entering and exiting desktop windowing mode. */ 3982 interface DesktopModeEntryExitTransitionListener { 3983 /** [transitionDuration] time it takes to run enter desktop mode transition */ 3984 fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) 3985 3986 /** [transitionDuration] time it takes to run exit desktop mode transition */ 3987 fun onExitDesktopModeTransitionStarted(transitionDuration: Int) 3988 } 3989 3990 /** The positions on a screen that a task can snap to. */ 3991 enum class SnapPosition { 3992 RIGHT, 3993 LEFT, 3994 } 3995 } 3996