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