• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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