<lambda>null1 package com.android.wm.shell.desktopmode
2 
3 import android.animation.Animator
4 import android.animation.AnimatorListenerAdapter
5 import android.animation.RectEvaluator
6 import android.animation.ValueAnimator
7 import android.app.ActivityManager.RunningTaskInfo
8 import android.app.ActivityOptions
9 import android.app.ActivityOptions.SourceInfo
10 import android.app.ActivityTaskManager.INVALID_TASK_ID
11 import android.app.PendingIntent
12 import android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
13 import android.app.PendingIntent.FLAG_MUTABLE
14 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
15 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
16 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
17 import android.content.Context
18 import android.content.Intent
19 import android.content.Intent.FILL_IN_COMPONENT
20 import android.graphics.PointF
21 import android.graphics.Rect
22 import android.os.Bundle
23 import android.os.IBinder
24 import android.os.SystemClock
25 import android.view.SurfaceControl
26 import android.view.WindowManager.TRANSIT_CLOSE
27 import android.window.TransitionInfo
28 import android.window.TransitionInfo.Change
29 import android.window.TransitionRequestInfo
30 import android.window.WindowContainerToken
31 import android.window.WindowContainerTransaction
32 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
33 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
34 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
35 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
36 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition
37 import com.android.wm.shell.protolog.ShellProtoLogGroup
38 import com.android.wm.shell.shared.TransitionUtil
39 import com.android.wm.shell.splitscreen.SplitScreenController
40 import com.android.wm.shell.transition.Transitions
41 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
42 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
43 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
44 import com.android.wm.shell.transition.Transitions.TransitionHandler
45 import com.android.wm.shell.util.KtProtoLog
46 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
47 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE
48 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
49 import java.util.function.Supplier
50 
51 /**
52  * Handles the transition to enter desktop from fullscreen by dragging on the handle bar. It also
53  * handles the cancellation case where the task is dragged back to the status bar area in the same
54  * gesture.
55  */
56 class DragToDesktopTransitionHandler(
57     private val context: Context,
58     private val transitions: Transitions,
59     private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
60     private val transactionSupplier: Supplier<SurfaceControl.Transaction>
61 ) : TransitionHandler {
62 
63     constructor(
64         context: Context,
65         transitions: Transitions,
66         rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
67     ) : this(
68         context,
69         transitions,
70         rootTaskDisplayAreaOrganizer,
71         Supplier { SurfaceControl.Transaction() }
72     )
73 
74     private val rectEvaluator = RectEvaluator(Rect())
75     private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
76 
77     private var dragToDesktopStateListener: DragToDesktopStateListener? = null
78     private lateinit var splitScreenController: SplitScreenController
79     private var transitionState: TransitionState? = null
80     private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener
81 
82     /** Whether a drag-to-desktop transition is in progress. */
83     val inProgress: Boolean
84         get() = transitionState != null
85 
86     /** The task id of the task currently being dragged from fullscreen/split. */
87     val draggingTaskId: Int
88         get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID
89     /** Sets a listener to receive callback about events during the transition animation. */
90     fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) {
91         dragToDesktopStateListener = listener
92     }
93 
94     /** Setter needed to avoid cyclic dependency. */
95     fun setSplitScreenController(controller: SplitScreenController) {
96         splitScreenController = controller
97     }
98 
99     fun setOnTaskResizeAnimatorListener(listener: OnTaskResizeAnimationListener) {
100         onTaskResizeAnimationListener = listener
101     }
102 
103     /**
104      * Starts a transition that performs a transient launch of Home so that Home is brought to the
105      * front while still keeping the currently focused task that is being dragged resumed. This
106      * allows the animation handler to reorder the task to the front and to scale it with the
107      * gesture into the desktop area with the Home and wallpaper behind it.
108      *
109      * Note that the transition handler for this transition doesn't call the finish callback until
110      * after one of the "end" or "cancel" transitions is merged into this transition.
111      */
112     fun startDragToDesktopTransition(
113         taskId: Int,
114         dragToDesktopAnimator: MoveToDesktopAnimator,
115     ) {
116         if (inProgress) {
117             KtProtoLog.v(
118                 ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
119                 "DragToDesktop: Drag to desktop transition already in progress."
120             )
121             return
122         }
123 
124         val options =
125             ActivityOptions.makeBasic().apply {
126                 setTransientLaunch()
127                 setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
128                 pendingIntentCreatorBackgroundActivityStartMode =
129                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
130             }
131         val pendingIntent =
132             PendingIntent.getActivity(
133                 context,
134                 0 /* requestCode */,
135                 launchHomeIntent,
136                 FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
137                 options.toBundle()
138             )
139         val wct = WindowContainerTransaction()
140         wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle())
141         val startTransitionToken =
142             transitions.startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
143 
144         transitionState =
145             if (isSplitTask(taskId)) {
146                 val otherTask =
147                     getOtherSplitTask(taskId)
148                         ?: throw IllegalStateException("Expected split task to have a counterpart.")
149                 TransitionState.FromSplit(
150                     draggedTaskId = taskId,
151                     dragAnimator = dragToDesktopAnimator,
152                     startTransitionToken = startTransitionToken,
153                     otherSplitTask = otherTask
154                 )
155             } else {
156                 TransitionState.FromFullscreen(
157                     draggedTaskId = taskId,
158                     dragAnimator = dragToDesktopAnimator,
159                     startTransitionToken = startTransitionToken
160                 )
161             }
162     }
163 
164     /**
165      * Starts a transition that "finishes" the drag to desktop gesture. This transition is intended
166      * to merge into the "start" transition and is the one that actually applies the bounds and
167      * windowing mode changes to the dragged task. This is called when the dragged task is released
168      * inside the desktop drop zone.
169      */
170     fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? {
171         if (!inProgress) {
172             // Don't attempt to finish a drag to desktop transition since there is no transition in
173             // progress which means that the drag to desktop transition was never successfully
174             // started.
175             return null
176         }
177         if (requireTransitionState().startAborted) {
178             // Don't attempt to complete the drag-to-desktop since the start transition didn't
179             // succeed as expected. Just reset the state as if nothing happened.
180             clearState()
181             return null
182         }
183         return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
184     }
185 
186     /**
187      * Starts a transition that "cancels" the drag to desktop gesture. This transition is intended
188      * to merge into the "start" transition and it restores the transient state that was used to
189      * launch the Home task over the dragged task. This is called when the dragged task is released
190      * outside the desktop drop zone and is instead dropped back into the status bar region that
191      * means the user wants to remain in their current windowing mode.
192      */
193     fun cancelDragToDesktopTransition(cancelState: CancelState) {
194         if (!inProgress) {
195             // Don't attempt to cancel a drag to desktop transition since there is no transition in
196             // progress which means that the drag to desktop transition was never successfully
197             // started.
198             return
199         }
200         val state = requireTransitionState()
201         if (state.startAborted) {
202             // Don't attempt to cancel the drag-to-desktop since the start transition didn't
203             // succeed as expected. Just reset the state as if nothing happened.
204             clearState()
205             return
206         }
207         state.cancelState = cancelState
208 
209         if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
210             // Regular case, transient launch of Home happened as is waiting for the cancel
211             // transient to start and merge. Animate the cancellation (scale back to original
212             // bounds) first before actually starting the cancel transition so that the wallpaper
213             // is visible behind the animating task.
214             startCancelAnimation()
215         } else if (
216             state.draggedTaskChange != null &&
217             (cancelState == CancelState.CANCEL_SPLIT_LEFT ||
218                     cancelState == CancelState.CANCEL_SPLIT_RIGHT)
219             ) {
220             // We have a valid dragged task, but the animation will be handled by
221             // SplitScreenController; request the transition here.
222             @SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) {
223                 SPLIT_POSITION_TOP_OR_LEFT
224             } else {
225                 SPLIT_POSITION_BOTTOM_OR_RIGHT
226             }
227             val wct = WindowContainerTransaction()
228             restoreWindowOrder(wct, state)
229             state.startTransitionFinishTransaction?.apply()
230             state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
231             requestSplitFromScaledTask(splitPosition, wct)
232             clearState()
233         } else {
234             // There's no dragged task, this can happen when the "cancel" happened too quickly
235             // before the "start" transition is even ready (like on a fling gesture). The
236             // "shrink" animation didn't even start, so there's no need to animate the "cancel".
237             // We also don't want to start the cancel transition yet since we don't have
238             // enough info to restore the order. We'll check for the cancelled state flag when
239             // the "start" animation is ready and cancel from #startAnimation instead.
240         }
241     }
242 
243     /** Calculate the bounds of a scaled task, then use those bounds to request split select. */
244     private fun requestSplitFromScaledTask(
245         @SplitPosition splitPosition: Int,
246         wct: WindowContainerTransaction
247     ) {
248         val state = requireTransitionState()
249         val taskInfo = state.draggedTaskChange?.taskInfo
250             ?: error("Expected non-null taskInfo")
251         val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds)
252         val taskScale = state.dragAnimator.scale
253         val scaledWidth = taskBounds.width() * taskScale
254         val scaledHeight = taskBounds.height() * taskScale
255         val dragPosition = PointF(state.dragAnimator.position)
256         state.dragAnimator.cancelAnimator()
257         val animatedTaskBounds = Rect(
258             dragPosition.x.toInt(),
259             dragPosition.y.toInt(),
260             (dragPosition.x + scaledWidth).toInt(),
261             (dragPosition.y + scaledHeight).toInt()
262         )
263         requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds)
264     }
265 
266     private fun requestSplitSelect(
267         wct: WindowContainerTransaction,
268         taskInfo: RunningTaskInfo,
269         @SplitPosition splitPosition: Int,
270         taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds)
271     ) {
272         // Prepare to exit split in order to enter split select.
273         if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
274             splitScreenController.prepareExitSplitScreen(
275                 wct,
276                 splitScreenController.getStageOfTask(taskInfo.taskId),
277                 SplitScreenController.EXIT_REASON_DESKTOP_MODE
278             )
279             splitScreenController.transitionHandler.onSplitToDesktop()
280         }
281         wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
282         wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi)
283         splitScreenController.requestEnterSplitSelect(
284             taskInfo,
285             wct,
286             splitPosition,
287             taskBounds
288         )
289     }
290 
291     override fun startAnimation(
292         transition: IBinder,
293         info: TransitionInfo,
294         startTransaction: SurfaceControl.Transaction,
295         finishTransaction: SurfaceControl.Transaction,
296         finishCallback: Transitions.TransitionFinishCallback
297     ): Boolean {
298         val state = requireTransitionState()
299 
300         val isStartDragToDesktop =
301             info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
302                 transition == state.startTransitionToken
303         if (!isStartDragToDesktop) {
304             return false
305         }
306 
307         // Layering: non-wallpaper, non-home tasks excluding the dragged task go at the bottom,
308         // then Home on top of that, wallpaper on top of that and finally the dragged task on top
309         // of everything.
310         val appLayers = info.changes.size
311         val homeLayers = info.changes.size * 2
312         val wallpaperLayers = info.changes.size * 3
313         val dragLayer = wallpaperLayers
314         val leafTaskFilter = TransitionUtil.LeafTaskFilter()
315         info.changes.withIndex().forEach { (i, change) ->
316             if (TransitionUtil.isWallpaper(change)) {
317                 val layer = wallpaperLayers - i
318                 startTransaction.apply {
319                     setLayer(change.leash, layer)
320                     show(change.leash)
321                 }
322             } else if (isHomeChange(change)) {
323                 state.homeToken = change.container
324                 val layer = homeLayers - i
325                 startTransaction.apply {
326                     setLayer(change.leash, layer)
327                     show(change.leash)
328                 }
329             } else if (TransitionInfo.isIndependent(change, info)) {
330                 // Root(s).
331                 when (state) {
332                     is TransitionState.FromSplit -> {
333                         state.splitRootChange = change
334                         val layer =
335                             if (state.cancelState == CancelState.NO_CANCEL) {
336                                 // Normal case, split root goes to the bottom behind everything
337                                 // else.
338                                 appLayers - i
339                             } else {
340                                 // Cancel-early case, pretend nothing happened so split root stays
341                                 // top.
342                                 dragLayer
343                             }
344                         startTransaction.apply {
345                             setLayer(change.leash, layer)
346                             show(change.leash)
347                         }
348                     }
349                     is TransitionState.FromFullscreen -> {
350                         // Most of the time we expect one change/task here, which should be the
351                         // same that initiated the drag and that should be layered on top of
352                         // everything.
353                         if (change.taskInfo?.taskId == state.draggedTaskId) {
354                             state.draggedTaskChange = change
355                             val bounds = change.endAbsBounds
356                             startTransaction.apply {
357                                 setLayer(change.leash, dragLayer)
358                                 setWindowCrop(change.leash, bounds.width(), bounds.height())
359                                 show(change.leash)
360                             }
361                         } else {
362                             // It's possible to see an additional change that isn't the dragged
363                             // task when the dragged task is translucent and so the task behind it
364                             // is included in the transition since it was visible and is now being
365                             // occluded by the Home task. Just layer it at the bottom and save it
366                             // in case we need to restore order if the drag is cancelled.
367                             state.otherRootChanges.add(change)
368                             val bounds = change.endAbsBounds
369                             startTransaction.apply {
370                                 setLayer(change.leash, appLayers - i)
371                                 setWindowCrop(change.leash, bounds.width(), bounds.height())
372                                 show(change.leash)
373                             }
374                         }
375                     }
376                 }
377             } else if (leafTaskFilter.test(change)) {
378                 // When dragging one of the split tasks, the dragged leaf needs to be re-parented
379                 // so that it can be layered separately from the rest of the split root/stages.
380                 // The split root including the other split side was layered behind the wallpaper
381                 // and home while the dragged split needs to be layered in front of them.
382                 // Do not do this in the cancel-early case though, since in that case nothing should
383                 // happen on screen so the layering will remain the same as if no transition
384                 // occurred.
385                 if (
386                     change.taskInfo?.taskId == state.draggedTaskId &&
387                     state.cancelState != CancelState.STANDARD_CANCEL
388                 ) {
389                     // We need access to the dragged task's change in both non-cancel and split
390                     // cancel cases.
391                     state.draggedTaskChange = change
392                 }
393                 if (
394                     change.taskInfo?.taskId == state.draggedTaskId &&
395                     state.cancelState == CancelState.NO_CANCEL
396                     ) {
397                     taskDisplayAreaOrganizer.reparentToDisplayArea(
398                         change.endDisplayId,
399                         change.leash,
400                         startTransaction
401                     )
402                     val bounds = change.endAbsBounds
403                     startTransaction.apply {
404                         setLayer(change.leash, dragLayer)
405                         setWindowCrop(change.leash, bounds.width(), bounds.height())
406                         show(change.leash)
407                     }
408                 }
409             }
410         }
411         state.startTransitionFinishCb = finishCallback
412         state.startTransitionFinishTransaction = finishTransaction
413         startTransaction.apply()
414 
415         if (state.cancelState == CancelState.NO_CANCEL) {
416             // Normal case, start animation to scale down the dragged task. It'll also be moved to
417             // follow the finger and when released we'll start the next phase/transition.
418             state.dragAnimator.startAnimation()
419         } else if (state.cancelState == CancelState.STANDARD_CANCEL) {
420             // Cancel-early case, the state was flagged was cancelled already, which means the
421             // gesture ended in the cancel region. This can happen even before the start transition
422             // is ready/animate here when cancelling quickly like with a fling. There's no point
423             // in starting the scale down animation that we would scale up anyway, so just jump
424             // directly into starting the cancel transition to restore WM order. Surfaces should
425             // not move as if no transition happened.
426             startCancelDragToDesktopTransition()
427         } else if (
428             state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
429             state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
430             ){
431             // Cancel-early case for split-cancel. The state was flagged already as a cancel for
432             // requesting split select. Similar to the above, this can happen due to quick fling
433             // gestures. We can simply request split here without needing to calculate animated
434             // task bounds as the task has not shrunk at all.
435             val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) {
436                 SPLIT_POSITION_TOP_OR_LEFT
437             } else {
438                 SPLIT_POSITION_BOTTOM_OR_RIGHT
439             }
440             val taskInfo = state.draggedTaskChange?.taskInfo
441                 ?: error("Expected non-null task info.")
442             val wct = WindowContainerTransaction()
443             restoreWindowOrder(wct)
444             state.startTransitionFinishTransaction?.apply()
445             state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
446             requestSplitSelect(wct, taskInfo, splitPosition)
447         }
448         return true
449     }
450 
451     override fun mergeAnimation(
452         transition: IBinder,
453         info: TransitionInfo,
454         t: SurfaceControl.Transaction,
455         mergeTarget: IBinder,
456         finishCallback: Transitions.TransitionFinishCallback
457     ) {
458         val state = requireTransitionState()
459         // We don't want to merge the split select animation if that's what we requested.
460         if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
461             state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) {
462             clearState()
463             return
464         }
465         val isCancelTransition =
466             info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
467                 transition == state.cancelTransitionToken &&
468                 mergeTarget == state.startTransitionToken
469         val isEndTransition =
470             info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
471                 mergeTarget == state.startTransitionToken
472 
473         val startTransactionFinishT =
474             state.startTransitionFinishTransaction
475                 ?: error("Start transition expected to be waiting for merge but wasn't")
476         val startTransitionFinishCb =
477             state.startTransitionFinishCb
478                 ?: error("Start transition expected to be waiting for merge but wasn't")
479         if (isEndTransition) {
480             info.changes.withIndex().forEach { (i, change) ->
481                 // If we're exiting split, hide the remaining split task.
482                 if (
483                     state is TransitionState.FromSplit &&
484                         change.taskInfo?.taskId == state.otherSplitTask
485                 ) {
486                     t.hide(change.leash)
487                     startTransactionFinishT.hide(change.leash)
488                 }
489                 if (change.mode == TRANSIT_CLOSE) {
490                     t.hide(change.leash)
491                     startTransactionFinishT.hide(change.leash)
492                 } else if (change.taskInfo?.taskId == state.draggedTaskId) {
493                     t.show(change.leash)
494                     startTransactionFinishT.show(change.leash)
495                     state.draggedTaskChange = change
496                 } else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) {
497                     // Other freeform tasks that are being restored go behind the dragged task.
498                     val draggedTaskLeash =
499                         state.draggedTaskChange?.leash
500                             ?: error("Expected dragged leash to be non-null")
501                     t.setRelativeLayer(change.leash, draggedTaskLeash, -i)
502                     startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i)
503                 }
504             }
505 
506             val draggedTaskChange =
507                 state.draggedTaskChange
508                     ?: throw IllegalStateException("Expected non-null change of dragged task")
509             val draggedTaskLeash = draggedTaskChange.leash
510             val startBounds = draggedTaskChange.startAbsBounds
511             val endBounds = draggedTaskChange.endAbsBounds
512 
513             // Pause any animation that may be currently playing; we will use the relevant
514             // details of that animation here.
515             state.dragAnimator.cancelAnimator()
516             // We still apply scale to task bounds; as we animate the bounds to their
517             // end value, animate scale to 1.
518             val startScale = state.dragAnimator.scale
519             val startPosition = state.dragAnimator.position
520             val unscaledStartWidth = startBounds.width()
521             val unscaledStartHeight = startBounds.height()
522             val unscaledStartBounds =
523                 Rect(
524                     startPosition.x.toInt(),
525                     startPosition.y.toInt(),
526                     startPosition.x.toInt() + unscaledStartWidth,
527                     startPosition.y.toInt() + unscaledStartHeight
528                 )
529 
530             dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t)
531             // Accept the merge by applying the merging transaction (applied by #showResizeVeil)
532             // and finish callback. Show the veil and position the task at the first frame before
533             // starting the final animation.
534             onTaskResizeAnimationListener.onAnimationStart(
535                 state.draggedTaskId,
536                 t,
537                 unscaledStartBounds
538             )
539             finishCallback.onTransitionFinished(null /* wct */)
540             val tx: SurfaceControl.Transaction = transactionSupplier.get()
541             ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds)
542                 .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
543                 .apply {
544                     addUpdateListener { animator ->
545                         val animBounds = animator.animatedValue as Rect
546                         val animFraction = animator.animatedFraction
547                         // Progress scale from starting value to 1 as animation plays.
548                         val animScale = startScale + animFraction * (1 - startScale)
549                         tx.apply {
550                             setScale(draggedTaskLeash, animScale, animScale)
551                             setPosition(
552                                 draggedTaskLeash,
553                                 animBounds.left.toFloat(),
554                                 animBounds.top.toFloat()
555                             )
556                             setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height())
557                         }
558                         onTaskResizeAnimationListener.onBoundsChange(
559                             state.draggedTaskId,
560                             tx,
561                             animBounds
562                         )
563                     }
564                     addListener(
565                         object : AnimatorListenerAdapter() {
566                             override fun onAnimationEnd(animation: Animator) {
567                                 onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
568                                 startTransitionFinishCb.onTransitionFinished(null /* null */)
569                                 clearState()
570                             }
571                         }
572                     )
573                     start()
574                 }
575         } else if (isCancelTransition) {
576             info.changes.forEach { change ->
577                 t.show(change.leash)
578                 startTransactionFinishT.show(change.leash)
579             }
580             t.apply()
581             finishCallback.onTransitionFinished(null /* wct */)
582             startTransitionFinishCb.onTransitionFinished(null /* wct */)
583             clearState()
584         }
585     }
586 
587     override fun handleRequest(
588         transition: IBinder,
589         request: TransitionRequestInfo
590     ): WindowContainerTransaction? {
591         // Only handle transitions started from shell.
592         return null
593     }
594 
595     override fun onTransitionConsumed(
596         transition: IBinder,
597         aborted: Boolean,
598         finishTransaction: SurfaceControl.Transaction?
599     ) {
600         val state = transitionState ?: return
601         if (aborted && state.startTransitionToken == transition) {
602             KtProtoLog.v(
603                 ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
604                 "DragToDesktop: onTransitionConsumed() start transition aborted"
605             )
606             state.startAborted = true
607         }
608     }
609 
610     private fun isHomeChange(change: Change): Boolean {
611         return change.taskInfo?.activityType == ACTIVITY_TYPE_HOME
612     }
613 
614     private fun startCancelAnimation() {
615         val state = requireTransitionState()
616         val dragToDesktopAnimator = state.dragAnimator
617 
618         val draggedTaskChange =
619             state.draggedTaskChange ?: throw IllegalStateException("Expected non-null task change")
620         val sc = draggedTaskChange.leash
621         // Pause the animation that shrinks the window when task is first dragged from fullscreen
622         dragToDesktopAnimator.cancelAnimator()
623         // Then animate the scaled window back to its original bounds.
624         val x: Float = dragToDesktopAnimator.position.x
625         val y: Float = dragToDesktopAnimator.position.y
626         val targetX = draggedTaskChange.endAbsBounds.left
627         val targetY = draggedTaskChange.endAbsBounds.top
628         val dx = targetX - x
629         val dy = targetY - y
630         val tx: SurfaceControl.Transaction = transactionSupplier.get()
631         ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
632             .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
633             .apply {
634                 addUpdateListener { animator ->
635                     val scale = animator.animatedValue as Float
636                     val fraction = animator.animatedFraction
637                     val animX = x + (dx * fraction)
638                     val animY = y + (dy * fraction)
639                     tx.apply {
640                         setPosition(sc, animX, animY)
641                         setScale(sc, scale, scale)
642                         show(sc)
643                         apply()
644                     }
645                 }
646                 addListener(
647                     object : AnimatorListenerAdapter() {
648                         override fun onAnimationEnd(animation: Animator) {
649                             dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx)
650                             // Start the cancel transition to restore order.
651                             startCancelDragToDesktopTransition()
652                         }
653                     }
654                 )
655                 start()
656             }
657     }
658 
659     private fun startCancelDragToDesktopTransition() {
660         val state = requireTransitionState()
661         val wct = WindowContainerTransaction()
662         restoreWindowOrder(wct, state)
663         state.cancelTransitionToken =
664             transitions.startTransition(
665                 TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this
666             )
667     }
668 
669     private fun restoreWindowOrder(
670         wct: WindowContainerTransaction,
671         state: TransitionState = requireTransitionState()
672     ) {
673         when (state) {
674             is TransitionState.FromFullscreen -> {
675                 // There may have been tasks sent behind home that are not the dragged task (like
676                 // when the dragged task is translucent and that makes the task behind it visible).
677                 // Restore the order of those first.
678                 state.otherRootChanges
679                     .mapNotNull { it.container }
680                     .forEach { wc ->
681                         // TODO(b/322852244): investigate why even though these "other" tasks are
682                         //  reordered in front of home and behind the translucent dragged task, its
683                         //  surface is not visible on screen.
684                         wct.reorder(wc, true /* toTop */)
685                     }
686                 val wc =
687                     state.draggedTaskChange?.container
688                         ?: error("Dragged task should be non-null before cancelling")
689                 // Then the dragged task a the very top.
690                 wct.reorder(wc, true /* toTop */)
691             }
692             is TransitionState.FromSplit -> {
693                 val wc =
694                     state.splitRootChange?.container
695                         ?: error("Split root should be non-null before cancelling")
696                 wct.reorder(wc, true /* toTop */)
697             }
698         }
699         val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling")
700         wct.restoreTransientOrder(homeWc)
701     }
702 
703     private fun clearState() {
704         transitionState = null
705     }
706 
707     private fun isSplitTask(taskId: Int): Boolean {
708         return splitScreenController.isTaskInSplitScreen(taskId)
709     }
710 
711     private fun getOtherSplitTask(taskId: Int): Int? {
712         val splitPos = splitScreenController.getSplitPosition(taskId)
713         if (splitPos == SPLIT_POSITION_UNDEFINED) return null
714         val otherTaskPos =
715             if (splitPos == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
716                 SPLIT_POSITION_TOP_OR_LEFT
717             } else {
718                 SPLIT_POSITION_BOTTOM_OR_RIGHT
719             }
720         return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
721     }
722 
723     private fun requireTransitionState(): TransitionState {
724         return transitionState ?: error("Expected non-null transition state")
725     }
726 
727     interface DragToDesktopStateListener {
728         fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction)
729 
730         fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction)
731     }
732 
733     sealed class TransitionState {
734         abstract val draggedTaskId: Int
735         abstract val dragAnimator: MoveToDesktopAnimator
736         abstract val startTransitionToken: IBinder
737         abstract var startTransitionFinishCb: Transitions.TransitionFinishCallback?
738         abstract var startTransitionFinishTransaction: SurfaceControl.Transaction?
739         abstract var cancelTransitionToken: IBinder?
740         abstract var homeToken: WindowContainerToken?
741         abstract var draggedTaskChange: Change?
742         abstract var cancelState: CancelState
743         abstract var startAborted: Boolean
744 
745         data class FromFullscreen(
746             override val draggedTaskId: Int,
747             override val dragAnimator: MoveToDesktopAnimator,
748             override val startTransitionToken: IBinder,
749             override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
750             override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
751             override var cancelTransitionToken: IBinder? = null,
752             override var homeToken: WindowContainerToken? = null,
753             override var draggedTaskChange: Change? = null,
754             override var cancelState: CancelState = CancelState.NO_CANCEL,
755             override var startAborted: Boolean = false,
756             var otherRootChanges: MutableList<Change> = mutableListOf()
757         ) : TransitionState()
758 
759         data class FromSplit(
760             override val draggedTaskId: Int,
761             override val dragAnimator: MoveToDesktopAnimator,
762             override val startTransitionToken: IBinder,
763             override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
764             override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
765             override var cancelTransitionToken: IBinder? = null,
766             override var homeToken: WindowContainerToken? = null,
767             override var draggedTaskChange: Change? = null,
768             override var cancelState: CancelState = CancelState.NO_CANCEL,
769             override var startAborted: Boolean = false,
770             var splitRootChange: Change? = null,
771             var otherSplitTask: Int
772         ) : TransitionState()
773     }
774 
775     /** Enum to provide context on cancelling a drag to desktop event. */
776     enum class CancelState {
777         /** No cancel case; this drag is not flagged for a cancel event. */
778         NO_CANCEL,
779         /** A standard cancel event; should restore task to previous windowing mode. */
780         STANDARD_CANCEL,
781         /** A cancel event where the task will request to enter split on the left side. */
782         CANCEL_SPLIT_LEFT,
783         /** A cancel event where the task will request to enter split on the right side. */
784         CANCEL_SPLIT_RIGHT
785     }
786 
787     companion object {
788         /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
789         private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
790     }
791 }
792