1 /*
2  * 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.R
20 import android.app.ActivityManager.RunningTaskInfo
21 import android.app.WindowConfiguration
22 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
23 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
24 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
25 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
26 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
27 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
28 import android.app.WindowConfiguration.WindowingMode
29 import android.content.Context
30 import android.content.res.TypedArray
31 import android.graphics.Point
32 import android.graphics.PointF
33 import android.graphics.Rect
34 import android.graphics.Region
35 import android.os.IBinder
36 import android.os.SystemProperties
37 import android.util.DisplayMetrics.DENSITY_DEFAULT
38 import android.view.SurfaceControl
39 import android.view.WindowManager.TRANSIT_CHANGE
40 import android.view.WindowManager.TRANSIT_NONE
41 import android.view.WindowManager.TRANSIT_OPEN
42 import android.view.WindowManager.TRANSIT_TO_FRONT
43 import android.window.TransitionInfo
44 import android.window.TransitionRequestInfo
45 import android.window.WindowContainerTransaction
46 import androidx.annotation.BinderThread
47 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
48 import com.android.wm.shell.ShellTaskOrganizer
49 import com.android.wm.shell.common.DisplayController
50 import com.android.wm.shell.common.ExecutorUtils
51 import com.android.wm.shell.common.ExternalInterfaceBinder
52 import com.android.wm.shell.common.LaunchAdjacentController
53 import com.android.wm.shell.common.RemoteCallable
54 import com.android.wm.shell.common.ShellExecutor
55 import com.android.wm.shell.common.SingleInstanceRemoteListener
56 import com.android.wm.shell.common.SyncTransactionQueue
57 import com.android.wm.shell.common.annotations.ExternalThread
58 import com.android.wm.shell.common.annotations.ShellMainThread
59 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
60 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
61 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
62 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.TO_DESKTOP_INDICATOR
63 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
64 import com.android.wm.shell.splitscreen.SplitScreenController
65 import com.android.wm.shell.sysui.ShellCommandHandler
66 import com.android.wm.shell.sysui.ShellController
67 import com.android.wm.shell.sysui.ShellInit
68 import com.android.wm.shell.sysui.ShellSharedConstants
69 import com.android.wm.shell.transition.Transitions
70 import com.android.wm.shell.util.KtProtoLog
71 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
72 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
73 import java.io.PrintWriter
74 import java.util.concurrent.Executor
75 import java.util.function.Consumer
76 
77 /** Handles moving tasks in and out of desktop */
78 class DesktopTasksController(
79         private val context: Context,
80         shellInit: ShellInit,
81         private val shellCommandHandler: ShellCommandHandler,
82         private val shellController: ShellController,
83         private val displayController: DisplayController,
84         private val shellTaskOrganizer: ShellTaskOrganizer,
85         private val syncQueue: SyncTransactionQueue,
86         private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
87         private val transitions: Transitions,
88         private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
89         private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
90         private val toggleResizeDesktopTaskTransitionHandler:
91         ToggleResizeDesktopTaskTransitionHandler,
92         private val desktopModeTaskRepository: DesktopModeTaskRepository,
93         private val launchAdjacentController: LaunchAdjacentController,
94         @ShellMainThread private val mainExecutor: ShellExecutor
95 ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
96 
97     private val desktopMode: DesktopModeImpl
98     private var visualIndicator: DesktopModeVisualIndicator? = null
99     private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> {
100         t: SurfaceControl.Transaction ->
101         visualIndicator?.releaseVisualIndicator(t)
102         visualIndicator = null
103     }
104     private val taskVisibilityListener = object : VisibleTasksListener {
105         override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
106             launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks
107         }
108     }
109 
110     private val transitionAreaHeight
111         get() = context.resources.getDimensionPixelSize(
112             com.android.wm.shell.R.dimen.desktop_mode_transition_area_height)
113 
114     private val transitionAreaWidth
115         get() = context.resources.getDimensionPixelSize(
116             com.android.wm.shell.R.dimen.desktop_mode_transition_area_width)
117 
118     // This is public to avoid cyclic dependency; it is set by SplitScreenController
119     lateinit var splitScreenController: SplitScreenController
120 
121     init {
122         desktopMode = DesktopModeImpl()
123         if (DesktopModeStatus.isProto2Enabled()) {
124             shellInit.addInitCallback({ onInit() }, this)
125         }
126     }
127 
128     private fun onInit() {
129         KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
130         shellCommandHandler.addDumpCallback(this::dump, this)
131         shellController.addExternalInterface(
132             ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE,
133             { createExternalInterface() },
134             this
135         )
136         transitions.addHandler(this)
137         desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor)
138     }
139 
140     /** Show all tasks, that are part of the desktop, on top of launcher */
141     fun showDesktopApps(displayId: Int) {
142         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps")
143         val wct = WindowContainerTransaction()
144         // TODO(b/278084491): pass in display id
145         bringDesktopAppsToFront(displayId, wct)
146 
147         // Execute transaction if there are pending operations
148         if (!wct.isEmpty) {
149             if (Transitions.ENABLE_SHELL_TRANSITIONS) {
150                 // TODO(b/268662477): add animation for the transition
151                 transitions.startTransition(TRANSIT_NONE, wct, null /* handler */)
152             } else {
153                 shellTaskOrganizer.applyTransaction(wct)
154             }
155         }
156     }
157 
158     /**
159      * Stash desktop tasks on display with id [displayId].
160      *
161      * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps
162      * launched in this state will be added to the desktop. Existing desktop tasks will be brought
163      * back to front during the launch.
164      */
165     fun stashDesktopApps(displayId: Int) {
166         if (DesktopModeStatus.isStashingEnabled()) {
167             KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps")
168             desktopModeTaskRepository.setStashed(displayId, true)
169         }
170     }
171 
172     /**
173      * Clear the stashed state for the given display
174      */
175     fun hideStashedDesktopApps(displayId: Int) {
176         if (DesktopModeStatus.isStashingEnabled()) {
177             KtProtoLog.v(
178                     WM_SHELL_DESKTOP_MODE,
179                     "DesktopTasksController: hideStashedApps displayId=%d",
180                     displayId
181             )
182             desktopModeTaskRepository.setStashed(displayId, false)
183         }
184     }
185 
186     /** Get number of tasks that are marked as visible */
187     fun getVisibleTaskCount(displayId: Int): Int {
188         return desktopModeTaskRepository.getVisibleTaskCount(displayId)
189     }
190 
191     /** Move a task with given `taskId` to desktop */
192     fun moveToDesktop(
193             decor: DesktopModeWindowDecoration,
194             taskId: Int,
195             wct: WindowContainerTransaction = WindowContainerTransaction()
196     ) {
197         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let {
198             task -> moveToDesktop(decor, task, wct)
199         }
200     }
201 
202     /**
203      * Move a task to desktop
204      */
205     fun moveToDesktop(
206             decor: DesktopModeWindowDecoration,
207             task: RunningTaskInfo,
208             wct: WindowContainerTransaction = WindowContainerTransaction()
209     ) {
210         KtProtoLog.v(
211             WM_SHELL_DESKTOP_MODE,
212             "DesktopTasksController: moveToDesktop taskId=%d",
213             task.taskId
214         )
215         // Bring other apps to front first
216         bringDesktopAppsToFront(task.displayId, wct)
217         addMoveToDesktopChanges(wct, task)
218 
219         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
220             enterDesktopTaskTransitionHandler.moveToDesktop(wct, decor)
221         } else {
222             shellTaskOrganizer.applyTransaction(wct)
223         }
224     }
225 
226     /**
227      * The first part of the animated move to desktop transition. Applies the changes to move task
228      * to desktop mode and sets the taskBounds to the passed in bounds, startBounds. This is
229      * followed with a call to {@link finishMoveToDesktop} or {@link cancelMoveToDesktop}.
230      */
231     fun startMoveToDesktop(
232             taskInfo: RunningTaskInfo,
233             startBounds: Rect,
234             dragToDesktopValueAnimator: MoveToDesktopAnimator
235     ) {
236         KtProtoLog.v(
237             WM_SHELL_DESKTOP_MODE,
238             "DesktopTasksController: startMoveToDesktop taskId=%d",
239             taskInfo.taskId
240         )
241         val wct = WindowContainerTransaction()
242         moveHomeTaskToFront(wct)
243         addMoveToDesktopChanges(wct, taskInfo)
244         wct.setBounds(taskInfo.token, startBounds)
245 
246         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
247             enterDesktopTaskTransitionHandler.startMoveToDesktop(wct, dragToDesktopValueAnimator,
248                     mOnAnimationFinishedCallback)
249         } else {
250             shellTaskOrganizer.applyTransaction(wct)
251         }
252     }
253 
254     /**
255      * The second part of the animated move to desktop transition, called after
256      * {@link startMoveToDesktop}. Brings apps to front and sets freeform task bounds.
257      */
258     private fun finalizeMoveToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
259         KtProtoLog.v(
260             WM_SHELL_DESKTOP_MODE,
261             "DesktopTasksController: finalizeMoveToDesktop taskId=%d",
262             taskInfo.taskId
263         )
264         val wct = WindowContainerTransaction()
265         bringDesktopAppsToFront(taskInfo.displayId, wct)
266         addMoveToDesktopChanges(wct, taskInfo)
267         wct.setBounds(taskInfo.token, freeformBounds)
268 
269         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
270             enterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct,
271                     mOnAnimationFinishedCallback)
272         } else {
273             shellTaskOrganizer.applyTransaction(wct)
274             releaseVisualIndicator()
275         }
276     }
277 
278     /**
279      * Perform needed cleanup transaction once animation is complete. Bounds need to be set
280      * here instead of initial wct to both avoid flicker and to have task bounds to use for
281      * the staging animation.
282      *
283      * @param taskInfo task entering split that requires a bounds update
284      */
285     fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
286         val wct = WindowContainerTransaction()
287         wct.setBounds(taskInfo.token, Rect())
288         shellTaskOrganizer.applyTransaction(wct)
289     }
290 
291     /** Move a task with given `taskId` to fullscreen */
292     fun moveToFullscreen(taskId: Int) {
293         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) }
294     }
295 
296     /** Move a task to fullscreen */
297     fun moveToFullscreen(task: RunningTaskInfo) {
298         KtProtoLog.v(
299             WM_SHELL_DESKTOP_MODE,
300             "DesktopTasksController: moveToFullscreen taskId=%d",
301             task.taskId
302         )
303 
304         val wct = WindowContainerTransaction()
305         addMoveToFullscreenChanges(wct, task)
306         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
307             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
308         } else {
309             shellTaskOrganizer.applyTransaction(wct)
310         }
311     }
312 
313     /** Move a desktop app to split screen. */
314     fun moveToSplit(task: RunningTaskInfo) {
315         KtProtoLog.v(
316             WM_SHELL_DESKTOP_MODE,
317             "DesktopTasksController: moveToSplit taskId=%d",
318             task.taskId
319         )
320         val wct = WindowContainerTransaction()
321         wct.setWindowingMode(task.token, WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW)
322         wct.setBounds(task.token, Rect())
323         wct.setDensityDpi(task.token, getDefaultDensityDpi())
324         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
325             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
326         } else {
327             shellTaskOrganizer.applyTransaction(wct)
328         }
329     }
330 
331     /**
332      * The second part of the animated move to desktop transition, called after
333      * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen
334      * and released back into status bar area.
335      */
336     fun cancelMoveToDesktop(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) {
337         KtProtoLog.v(
338             WM_SHELL_DESKTOP_MODE,
339             "DesktopTasksController: cancelMoveToDesktop taskId=%d",
340             task.taskId
341         )
342         val wct = WindowContainerTransaction()
343         wct.setBounds(task.token, Rect())
344 
345         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
346             enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(wct,
347                     moveToDesktopAnimator) { t ->
348                 val callbackWCT = WindowContainerTransaction()
349                 visualIndicator?.releaseVisualIndicator(t)
350                 visualIndicator = null
351                 addMoveToFullscreenChanges(callbackWCT, task)
352                 transitions.startTransition(TRANSIT_CHANGE, callbackWCT, null /* handler */)
353             }
354         } else {
355             addMoveToFullscreenChanges(wct, task)
356             shellTaskOrganizer.applyTransaction(wct)
357             releaseVisualIndicator()
358         }
359     }
360 
361     private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) {
362         KtProtoLog.v(
363                 WM_SHELL_DESKTOP_MODE,
364                 "DesktopTasksController: moveToFullscreen with animation taskId=%d",
365                 task.taskId
366         )
367         val wct = WindowContainerTransaction()
368         addMoveToFullscreenChanges(wct, task)
369 
370         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
371             exitDesktopTaskTransitionHandler.startTransition(
372             Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct, position, mOnAnimationFinishedCallback)
373         } else {
374             shellTaskOrganizer.applyTransaction(wct)
375             releaseVisualIndicator()
376         }
377     }
378 
379     /** Move a task to the front */
380     fun moveTaskToFront(taskId: Int) {
381         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) }
382     }
383 
384     /** Move a task to the front */
385     fun moveTaskToFront(taskInfo: RunningTaskInfo) {
386         KtProtoLog.v(
387             WM_SHELL_DESKTOP_MODE,
388             "DesktopTasksController: moveTaskToFront taskId=%d",
389             taskInfo.taskId
390         )
391 
392         val wct = WindowContainerTransaction()
393         wct.reorder(taskInfo.token, true)
394         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
395             transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
396         } else {
397             shellTaskOrganizer.applyTransaction(wct)
398         }
399     }
400 
401     /**
402      * Move task to the next display.
403      *
404      * Queries all current known display ids and sorts them in ascending order. Then iterates
405      * through the list and looks for the display id that is larger than the display id for
406      * the passed in task. If a display with a higher id is not found, iterates through the list and
407      * finds the first display id that is not the display id for the passed in task.
408      *
409      * If a display matching the above criteria is found, re-parents the task to that display.
410      * No-op if no such display is found.
411      */
412     fun moveToNextDisplay(taskId: Int) {
413         val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
414         if (task == null) {
415             KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId)
416             return
417         }
418         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d",
419                 taskId, task.displayId)
420 
421         val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted()
422         // Get the first display id that is higher than current task display id
423         var newDisplayId = displayIds.firstOrNull { displayId -> displayId > task.displayId }
424         if (newDisplayId == null) {
425             // No display with a higher id, get the first display id that is not the task display id
426             newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId }
427         }
428         if (newDisplayId == null) {
429             KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found")
430             return
431         }
432         moveToDisplay(task, newDisplayId)
433     }
434 
435     /**
436      * Move [task] to display with [displayId].
437      *
438      * No-op if task is already on that display per [RunningTaskInfo.displayId].
439      */
440     private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) {
441         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d",
442                 task.taskId, displayId)
443 
444         if (task.displayId == displayId) {
445             KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display")
446             return
447         }
448 
449         val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
450         if (displayAreaInfo == null) {
451             KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found")
452             return
453         }
454 
455         val wct = WindowContainerTransaction()
456         wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
457         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
458             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
459         } else {
460             shellTaskOrganizer.applyTransaction(wct)
461         }
462     }
463 
464     /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */
465     fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo, windowDecor: DesktopModeWindowDecoration) {
466         val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
467 
468         val stableBounds = Rect()
469         displayLayout.getStableBounds(stableBounds)
470         val destinationBounds = Rect()
471         if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) {
472             // The desktop task is currently occupying the whole stable bounds, toggle to the
473             // default bounds.
474             getDefaultDesktopTaskBounds(
475                 density = taskInfo.configuration.densityDpi.toFloat() / DENSITY_DEFAULT,
476                 stableBounds = stableBounds,
477                 outBounds = destinationBounds
478             )
479         } else {
480             // Toggle to the stable bounds.
481             destinationBounds.set(stableBounds)
482         }
483 
484         val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
485         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
486             toggleResizeDesktopTaskTransitionHandler.startTransition(
487                 wct,
488                 taskInfo.taskId,
489                 windowDecor
490             )
491         } else {
492             shellTaskOrganizer.applyTransaction(wct)
493         }
494     }
495 
496     private fun getDefaultDesktopTaskBounds(density: Float, stableBounds: Rect, outBounds: Rect) {
497         val width = (DESKTOP_MODE_DEFAULT_WIDTH_DP * density + 0.5f).toInt()
498         val height = (DESKTOP_MODE_DEFAULT_HEIGHT_DP * density + 0.5f).toInt()
499         outBounds.set(0, 0, width, height)
500         // Center the task in stable bounds
501         outBounds.offset(
502             stableBounds.centerX() - outBounds.centerX(),
503             stableBounds.centerY() - outBounds.centerY()
504         )
505     }
506 
507     /**
508      * Get windowing move for a given `taskId`
509      *
510      * @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found
511      */
512     @WindowingMode
513     fun getTaskWindowingMode(taskId: Int): Int {
514         return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode
515             ?: WINDOWING_MODE_UNDEFINED
516     }
517 
518     private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) {
519         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront")
520         val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId)
521 
522         // First move home to front and then other tasks on top of it
523         moveHomeTaskToFront(wct)
524 
525         val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder()
526         activeTasks
527             // Sort descending as the top task is at index 0. It should be ordered to top last
528             .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) }
529             .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
530             .forEach { task -> wct.reorder(task.token, true /* onTop */) }
531     }
532 
533     private fun moveHomeTaskToFront(wct: WindowContainerTransaction) {
534         shellTaskOrganizer
535             .getRunningTasks(context.displayId)
536             .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME }
537             ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) }
538     }
539 
540     private fun releaseVisualIndicator() {
541         val t = SurfaceControl.Transaction()
542         visualIndicator?.releaseVisualIndicator(t)
543         visualIndicator = null
544         syncQueue.runInSync { transaction ->
545             transaction.merge(t)
546             t.close()
547         }
548     }
549 
550     override fun getContext(): Context {
551         return context
552     }
553 
554     override fun getRemoteCallExecutor(): ShellExecutor {
555         return mainExecutor
556     }
557 
558     override fun startAnimation(
559         transition: IBinder,
560         info: TransitionInfo,
561         startTransaction: SurfaceControl.Transaction,
562         finishTransaction: SurfaceControl.Transaction,
563         finishCallback: Transitions.TransitionFinishCallback
564     ): Boolean {
565         // This handler should never be the sole handler, so should not animate anything.
566         return false
567     }
568 
569     override fun handleRequest(
570         transition: IBinder,
571         request: TransitionRequestInfo
572     ): WindowContainerTransaction? {
573         KtProtoLog.v(
574             WM_SHELL_DESKTOP_MODE,
575             "DesktopTasksController: handleRequest request=%s",
576             request
577         )
578         // Check if we should skip handling this transition
579         var reason = ""
580         val triggerTask = request.triggerTask
581         val shouldHandleRequest =
582             when {
583                 // Only handle open or to front transitions
584                 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> {
585                     reason = "transition type not handled (${request.type})"
586                     false
587                 }
588                 // Only handle when it is a task transition
589                 triggerTask == null -> {
590                     reason = "triggerTask is null"
591                     false
592                 }
593                 // Only handle standard type tasks
594                 triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> {
595                     reason = "activityType not handled (${triggerTask.activityType})"
596                     false
597                 }
598                 // Only handle fullscreen or freeform tasks
599                 triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN &&
600                         triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
601                     reason = "windowingMode not handled (${triggerTask.windowingMode})"
602                     false
603                 }
604                 // Otherwise process it
605                 else -> true
606             }
607 
608         if (!shouldHandleRequest) {
609             KtProtoLog.v(
610                     WM_SHELL_DESKTOP_MODE,
611                     "DesktopTasksController: skipping handleRequest reason=%s",
612                     reason
613             )
614             return null
615         }
616 
617         val result = triggerTask?.let { task ->
618             when {
619                 // If display has tasks stashed, handle as stashed launch
620                 desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task)
621                 // Check if fullscreen task should be updated
622                 task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task)
623                 // Check if freeform task should be updated
624                 task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task)
625                 else -> {
626                     null
627                 }
628             }
629         }
630         KtProtoLog.v(
631                 WM_SHELL_DESKTOP_MODE,
632                 "DesktopTasksController: handleRequest result=%s",
633                 result ?: "null"
634         )
635         return result
636     }
637 
638     /**
639      * Applies the proper surface states (rounded corners) to tasks when desktop mode is active.
640      * This is intended to be used when desktop mode is part of another animation but isn't, itself,
641      * animating.
642      */
643     fun syncSurfaceState(
644             info: TransitionInfo,
645             finishTransaction: SurfaceControl.Transaction
646     ) {
647         // Add rounded corners to freeform windows
648         val ta: TypedArray = context.obtainStyledAttributes(
649                 intArrayOf(R.attr.dialogCornerRadius))
650         val cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat()
651         ta.recycle()
652         info.changes
653                 .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
654                 .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
655     }
656 
657     private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
658         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch")
659         val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
660         if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
661             KtProtoLog.d(
662                     WM_SHELL_DESKTOP_MODE,
663                     "DesktopTasksController: switch freeform task to fullscreen oon transition" +
664                             " taskId=%d",
665                     task.taskId
666             )
667             return WindowContainerTransaction().also { wct ->
668                 addMoveToFullscreenChanges(wct, task)
669             }
670         }
671         return null
672     }
673 
674     private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
675         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch")
676         val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
677         if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
678             KtProtoLog.d(
679                     WM_SHELL_DESKTOP_MODE,
680                     "DesktopTasksController: switch fullscreen task to freeform on transition" +
681                             " taskId=%d",
682                     task.taskId
683             )
684             return WindowContainerTransaction().also { wct ->
685                 addMoveToDesktopChanges(wct, task)
686             }
687         }
688         return null
689     }
690 
691     private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction {
692         KtProtoLog.d(
693                 WM_SHELL_DESKTOP_MODE,
694                 "DesktopTasksController: launch apps with stashed on transition taskId=%d",
695                 task.taskId
696         )
697         val wct = WindowContainerTransaction()
698         bringDesktopAppsToFront(task.displayId, wct)
699         addMoveToDesktopChanges(wct, task)
700         desktopModeTaskRepository.setStashed(task.displayId, false)
701         return wct
702     }
703 
704     private fun addMoveToDesktopChanges(
705         wct: WindowContainerTransaction,
706         taskInfo: RunningTaskInfo
707     ) {
708         val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
709         val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FREEFORM) {
710             // Display windowing is freeform, set to undefined and inherit it
711             WINDOWING_MODE_UNDEFINED
712         } else {
713             WINDOWING_MODE_FREEFORM
714         }
715         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
716         wct.reorder(taskInfo.token, true /* onTop */)
717         if (isDesktopDensityOverrideSet()) {
718             wct.setDensityDpi(taskInfo.token, getDesktopDensityDpi())
719         }
720     }
721 
722     private fun addMoveToFullscreenChanges(
723         wct: WindowContainerTransaction,
724         taskInfo: RunningTaskInfo
725     ) {
726         val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
727         val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
728             // Display windowing is fullscreen, set to undefined and inherit it
729             WINDOWING_MODE_UNDEFINED
730         } else {
731             WINDOWING_MODE_FULLSCREEN
732         }
733         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
734         wct.setBounds(taskInfo.token, Rect())
735         if (isDesktopDensityOverrideSet()) {
736             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
737         }
738     }
739 
740     /**
741      * Adds split screen changes to a transaction. Note that bounds are not reset here due to
742      * animation; see {@link onDesktopSplitSelectAnimComplete}
743      */
744     private fun addMoveToSplitChanges(
745         wct: WindowContainerTransaction,
746         taskInfo: RunningTaskInfo
747     ) {
748         wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
749         // The task's density may have been overridden in freeform; revert it here as we don't
750         // want it overridden in multi-window.
751         wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
752     }
753 
754     /**
755      * Requests a task be transitioned from desktop to split select. Applies needed windowing
756      * changes if this transition is enabled.
757      */
758     fun requestSplit(
759         taskInfo: RunningTaskInfo
760     ) {
761         val windowingMode = taskInfo.windowingMode
762         if (windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM
763         ) {
764             val wct = WindowContainerTransaction()
765             addMoveToSplitChanges(wct, taskInfo)
766             splitScreenController.requestEnterSplitSelect(taskInfo, wct,
767                 SPLIT_POSITION_BOTTOM_OR_RIGHT, taskInfo.configuration.windowConfiguration.bounds)
768         }
769     }
770 
771     private fun getDefaultDensityDpi(): Int {
772         return context.resources.displayMetrics.densityDpi
773     }
774 
775     private fun getDesktopDensityDpi(): Int {
776         return DESKTOP_DENSITY_OVERRIDE
777     }
778 
779     /** Creates a new instance of the external interface to pass to another process. */
780     private fun createExternalInterface(): ExternalInterfaceBinder {
781         return IDesktopModeImpl(this)
782     }
783 
784     /** Get connection interface between sysui and shell */
785     fun asDesktopMode(): DesktopMode {
786         return desktopMode
787     }
788 
789     /**
790      * Perform checks required on drag move. Create/release fullscreen indicator as needed.
791      * Different sources for x and y coordinates are used due to different needs for each:
792      * We want split transitions to be based on input coordinates but fullscreen transition
793      * to be based on task edge coordinate.
794      *
795      * @param taskInfo the task being dragged.
796      * @param taskSurface SurfaceControl of dragged task.
797      * @param inputCoordinate coordinates of input. Used for checks against left/right edge of screen.
798      * @param taskBounds bounds of dragged task. Used for checks against status bar height.
799      */
800     fun onDragPositioningMove(
801         taskInfo: RunningTaskInfo,
802         taskSurface: SurfaceControl,
803         inputCoordinate: PointF,
804         taskBounds: Rect
805     ) {
806         val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
807         if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
808         var type = DesktopModeVisualIndicator.determineIndicatorType(inputCoordinate,
809             taskBounds, displayLayout, context)
810         if (type != DesktopModeVisualIndicator.INVALID_INDICATOR && visualIndicator == null) {
811             visualIndicator = DesktopModeVisualIndicator(
812                 syncQueue, taskInfo,
813                 displayController, context, taskSurface, shellTaskOrganizer,
814                 rootTaskDisplayAreaOrganizer, type)
815             visualIndicator?.createIndicatorWithAnimatedBounds()
816             return
817         }
818         if (visualIndicator?.eventOutsideRange(inputCoordinate.x,
819                 taskBounds.top.toFloat()) == true) {
820             releaseVisualIndicator()
821         }
822     }
823 
824     /**
825      * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area.
826      *
827      * @param taskInfo the task being dragged.
828      * @param position position of surface when drag ends.
829      * @param inputCoordinate the coordinates of the motion event
830      * @param taskBounds the updated bounds of the task being dragged.
831      * @param windowDecor the window decoration for the task being dragged
832      */
833     fun onDragPositioningEnd(
834         taskInfo: RunningTaskInfo,
835         position: Point,
836         inputCoordinate: PointF,
837         taskBounds: Rect,
838         windowDecor: DesktopModeWindowDecoration
839     ) {
840         if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
841             return
842         }
843         if (taskBounds.top <= transitionAreaHeight) {
844             windowDecor.incrementRelayoutBlock()
845             moveToFullscreenWithAnimation(taskInfo, position)
846         }
847         if (inputCoordinate.x <= transitionAreaWidth) {
848             releaseVisualIndicator()
849             var wct = WindowContainerTransaction()
850             addMoveToSplitChanges(wct, taskInfo)
851             splitScreenController.requestEnterSplitSelect(taskInfo, wct,
852                 SPLIT_POSITION_TOP_OR_LEFT, taskBounds)
853         }
854         if (inputCoordinate.x >= (displayController.getDisplayLayout(taskInfo.displayId)?.width()
855             ?.minus(transitionAreaWidth) ?: return)) {
856             releaseVisualIndicator()
857             var wct = WindowContainerTransaction()
858             addMoveToSplitChanges(wct, taskInfo)
859             splitScreenController.requestEnterSplitSelect(taskInfo, wct,
860                 SPLIT_POSITION_BOTTOM_OR_RIGHT, taskBounds)
861         }
862     }
863 
864     /**
865      * Perform checks required on drag move. Create/release fullscreen indicator and transitions
866      * indicator to freeform or fullscreen dimensions as needed.
867      *
868      * @param taskInfo the task being dragged.
869      * @param taskSurface SurfaceControl of dragged task.
870      * @param y coordinate of dragged task. Used for checks against status bar height.
871      */
872     fun onDragPositioningMoveThroughStatusBar(
873             taskInfo: RunningTaskInfo,
874             taskSurface: SurfaceControl,
875             y: Float
876     ) {
877         // If the motion event is above the status bar and the visual indicator is not yet visible,
878         // return since we do not need to show the visual indicator at this point.
879         if (y < getStatusBarHeight(taskInfo) && visualIndicator == null) {
880             return
881         }
882         if (visualIndicator == null) {
883             visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo,
884                     displayController, context, taskSurface, shellTaskOrganizer,
885                     rootTaskDisplayAreaOrganizer, TO_DESKTOP_INDICATOR)
886             visualIndicator?.createIndicatorWithAnimatedBounds()
887         }
888         val indicator = visualIndicator ?: return
889         if (y >= getFreeformTransitionStatusBarDragThreshold(taskInfo)) {
890             if (indicator.isFullscreen) {
891                 indicator.transitionFullscreenIndicatorToFreeform()
892             }
893         } else if (!indicator.isFullscreen) {
894             indicator.transitionFreeformIndicatorToFullscreen()
895         }
896     }
897 
898     /**
899      * Perform checks required when drag ends under status bar area.
900      *
901      * @param taskInfo the task being dragged.
902      * @param y height of drag, to be checked against status bar height.
903      */
904     fun onDragPositioningEndThroughStatusBar(
905             taskInfo: RunningTaskInfo,
906             freeformBounds: Rect
907     ) {
908         finalizeMoveToDesktop(taskInfo, freeformBounds)
909     }
910 
911     private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int {
912         return displayController.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0
913     }
914 
915     /**
916      * Returns the threshold at which we transition a task into freeform when dragging a
917      * fullscreen task down from the status bar
918      */
919     private fun getFreeformTransitionStatusBarDragThreshold(taskInfo: RunningTaskInfo): Int {
920         return 2 * getStatusBarHeight(taskInfo)
921     }
922 
923     /**
924      * Update the corner region for a specified task
925      */
926     fun onTaskCornersChanged(taskId: Int, corner: Region) {
927         desktopModeTaskRepository.updateTaskCorners(taskId, corner)
928     }
929 
930     /**
931      * Remove a previously tracked corner region for a specified task.
932      */
933     fun removeCornersForTask(taskId: Int) {
934         desktopModeTaskRepository.removeTaskCorners(taskId)
935     }
936 
937     /**
938      * Adds a listener to find out about changes in the visibility of freeform tasks.
939      *
940      * @param listener the listener to add.
941      * @param callbackExecutor the executor to call the listener on.
942      */
943     fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
944         desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor)
945     }
946 
947     /**
948      * Adds a listener to track changes to desktop task corners
949      *
950      * @param listener the listener to add.
951      * @param callbackExecutor the executor to call the listener on.
952      */
953     fun setTaskCornerListener(
954             listener: Consumer<Region>,
955             callbackExecutor: Executor
956     ) {
957         desktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor)
958     }
959 
960     private fun dump(pw: PrintWriter, prefix: String) {
961         val innerPrefix = "$prefix  "
962         pw.println("${prefix}DesktopTasksController")
963         desktopModeTaskRepository.dump(pw, innerPrefix)
964     }
965 
966     /** The interface for calls from outside the shell, within the host process. */
967     @ExternalThread
968     private inner class DesktopModeImpl : DesktopMode {
969         override fun addVisibleTasksListener(
970                 listener: VisibleTasksListener,
971                 callbackExecutor: Executor
972         ) {
973             mainExecutor.execute {
974                 this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor)
975             }
976         }
977 
978         override fun addDesktopGestureExclusionRegionListener(
979                 listener: Consumer<Region>,
980                 callbackExecutor: Executor
981         ) {
982             mainExecutor.execute {
983                 this@DesktopTasksController.setTaskCornerListener(listener, callbackExecutor)
984             }
985         }
986     }
987 
988     /** The interface for calls from outside the host process. */
989     @BinderThread
990     private class IDesktopModeImpl(private var controller: DesktopTasksController?) :
991         IDesktopMode.Stub(), ExternalInterfaceBinder {
992 
993         private lateinit var remoteListener:
994                 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
995 
996         private val listener: VisibleTasksListener = object : VisibleTasksListener {
997             override fun onVisibilityChanged(displayId: Int, visible: Boolean) {
998                 KtProtoLog.v(
999                         WM_SHELL_DESKTOP_MODE,
1000                         "IDesktopModeImpl: onVisibilityChanged display=%d visible=%b",
1001                         displayId,
1002                         visible
1003                 )
1004                 remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) }
1005             }
1006 
1007             override fun onStashedChanged(displayId: Int, stashed: Boolean) {
1008                 KtProtoLog.v(
1009                         WM_SHELL_DESKTOP_MODE,
1010                         "IDesktopModeImpl: onStashedChanged display=%d stashed=%b",
1011                         displayId,
1012                         stashed
1013                 )
1014                 remoteListener.call { l -> l.onStashedChanged(displayId, stashed) }
1015             }
1016         }
1017 
1018         init {
1019             remoteListener =
1020                     SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
1021                             controller,
1022                             { c ->
1023                                 c.desktopModeTaskRepository.addVisibleTasksListener(
1024                                         listener,
1025                                         c.mainExecutor
1026                                 )
1027                             },
1028                             { c ->
1029                                 c.desktopModeTaskRepository.removeVisibleTasksListener(listener)
1030                             }
1031                     )
1032         }
1033 
1034         /** Invalidates this instance, preventing future calls from updating the controller. */
1035         override fun invalidate() {
1036             remoteListener.unregister()
1037             controller = null
1038         }
1039 
1040         override fun showDesktopApps(displayId: Int) {
1041             ExecutorUtils.executeRemoteCallWithTaskPermission(
1042                 controller,
1043                 "showDesktopApps"
1044             ) { c -> c.showDesktopApps(displayId) }
1045         }
1046 
1047         override fun stashDesktopApps(displayId: Int) {
1048             ExecutorUtils.executeRemoteCallWithTaskPermission(
1049                     controller,
1050                     "stashDesktopApps"
1051             ) { c -> c.stashDesktopApps(displayId) }
1052         }
1053 
1054         override fun hideStashedDesktopApps(displayId: Int) {
1055             ExecutorUtils.executeRemoteCallWithTaskPermission(
1056                     controller,
1057                     "hideStashedDesktopApps"
1058             ) { c -> c.hideStashedDesktopApps(displayId) }
1059         }
1060 
1061         override fun showDesktopApp(taskId: Int) {
1062             ExecutorUtils.executeRemoteCallWithTaskPermission(
1063                     controller,
1064                     "showDesktopApp"
1065             ) { c -> c.moveTaskToFront(taskId) }
1066         }
1067 
1068         override fun getVisibleTaskCount(displayId: Int): Int {
1069             val result = IntArray(1)
1070             ExecutorUtils.executeRemoteCallWithTaskPermission(
1071                 controller,
1072                 "getVisibleTaskCount",
1073                 { controller -> result[0] = controller.getVisibleTaskCount(displayId) },
1074                 true /* blocking */
1075             )
1076             return result[0]
1077         }
1078 
1079         override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
1080             ExecutorUtils.executeRemoteCallWithTaskPermission(
1081                 controller,
1082                 "onDesktopSplitSelectAnimComplete"
1083             ) { c -> c.onDesktopSplitSelectAnimComplete(taskInfo) }
1084         }
1085 
1086         override fun setTaskListener(listener: IDesktopTaskListener?) {
1087             KtProtoLog.v(
1088                     WM_SHELL_DESKTOP_MODE,
1089                     "IDesktopModeImpl: set task listener=%s",
1090                     listener ?: "null"
1091             )
1092             ExecutorUtils.executeRemoteCallWithTaskPermission(
1093                     controller,
1094                     "setTaskListener"
1095             ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() }
1096         }
1097     }
1098 
1099     companion object {
1100         private val DESKTOP_DENSITY_OVERRIDE =
1101             SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284)
1102         private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000)
1103 
1104         // Override default freeform task width when desktop mode is enabled. In dips.
1105         private val DESKTOP_MODE_DEFAULT_WIDTH_DP =
1106             SystemProperties.getInt("persist.wm.debug.desktop_mode.default_width", 840)
1107 
1108         // Override default freeform task height when desktop mode is enabled. In dips.
1109         private val DESKTOP_MODE_DEFAULT_HEIGHT_DP =
1110             SystemProperties.getInt("persist.wm.debug.desktop_mode.default_height", 630)
1111 
1112         /**
1113          * Check if desktop density override is enabled
1114          */
1115         @JvmStatic
1116         fun isDesktopDensityOverrideSet(): Boolean {
1117             return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE
1118         }
1119     }
1120 }
1121