• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.android.wm.shell.desktopmode
2 
3 import android.animation.Animator
4 import android.animation.AnimatorListenerAdapter
5 import android.animation.AnimatorSet
6 import android.animation.RectEvaluator
7 import android.animation.ValueAnimator
8 import android.app.ActivityManager.RunningTaskInfo
9 import android.app.ActivityOptions
10 import android.app.ActivityOptions.SourceInfo
11 import android.app.ActivityTaskManager.INVALID_TASK_ID
12 import android.app.PendingIntent
13 import android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
14 import android.app.PendingIntent.FLAG_MUTABLE
15 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
16 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
17 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
18 import android.content.Context
19 import android.content.Intent
20 import android.content.Intent.FILL_IN_COMPONENT
21 import android.graphics.PointF
22 import android.graphics.Rect
23 import android.os.IBinder
24 import android.os.SystemClock
25 import android.os.SystemProperties
26 import android.os.UserHandle
27 import android.view.Choreographer
28 import android.view.SurfaceControl
29 import android.view.SurfaceControl.Transaction
30 import android.view.WindowManager.TRANSIT_CLOSE
31 import android.window.DesktopModeFlags
32 import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX
33 import android.window.TransitionInfo
34 import android.window.TransitionInfo.Change
35 import android.window.TransitionRequestInfo
36 import android.window.WindowContainerTransaction
37 import com.android.internal.annotations.VisibleForTesting
38 import com.android.internal.dynamicanimation.animation.SpringForce
39 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
40 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
41 import com.android.internal.jank.InteractionJankMonitor
42 import com.android.internal.protolog.ProtoLog
43 import com.android.internal.util.LatencyTracker
44 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
45 import com.android.wm.shell.animation.FloatProperties
46 import com.android.wm.shell.bubbles.BubbleController
47 import com.android.wm.shell.bubbles.BubbleTransitions
48 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
49 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
50 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
51 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
52 import com.android.wm.shell.shared.TransitionUtil
53 import com.android.wm.shell.shared.animation.Interpolators
54 import com.android.wm.shell.shared.animation.PhysicsAnimator
55 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
56 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
57 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
58 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition
59 import com.android.wm.shell.splitscreen.SplitScreenController
60 import com.android.wm.shell.transition.Transitions
61 import com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE
62 import com.android.wm.shell.transition.Transitions.TransitionHandler
63 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
64 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE
65 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
66 import java.util.Optional
67 import java.util.function.Supplier
68 import kotlin.math.max
69 
70 /**
71  * Handles the transition to enter desktop from fullscreen by dragging on the handle bar. It also
72  * handles the cancellation case where the task is dragged back to the status bar area in the same
73  * gesture.
74  *
75  * It's a base sealed class that delegates flag dependant logic to its subclasses:
76  * [DefaultDragToDesktopTransitionHandler] and [SpringDragToDesktopTransitionHandler]
77  *
78  * TODO(b/356764679): Clean up after the full flag rollout
79  */
80 sealed class DragToDesktopTransitionHandler(
81     private val context: Context,
82     private val transitions: Transitions,
83     private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
84     private val desktopUserRepositories: DesktopUserRepositories,
85     protected val interactionJankMonitor: InteractionJankMonitor,
86     private val bubbleController: Optional<BubbleController>,
87     protected val transactionSupplier: Supplier<SurfaceControl.Transaction>,
88 ) : TransitionHandler {
89 
90     protected val rectEvaluator = RectEvaluator(Rect())
91     private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
92 
93     private lateinit var splitScreenController: SplitScreenController
94     private var transitionState: TransitionState? = null
95 
96     /** Whether a drag-to-desktop transition is in progress. */
97     val inProgress: Boolean
98         get() = transitionState != null
99 
100     /** The task id of the task currently being dragged from fullscreen/split. */
101     val draggingTaskId: Int
102         get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID
103 
104     /** Listener to receive callback about events during the transition animation. */
105     var dragToDesktopStateListener: DragToDesktopStateListener? = null
106 
107     /** Task listener for animation start, task bounds resize, and the animation finish */
108     lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener
109 
110     /** Setter needed to avoid cyclic dependency. */
111     fun setSplitScreenController(controller: SplitScreenController) {
112         splitScreenController = controller
113     }
114 
115     /**
116      * Starts a transition that performs a transient launch of Home so that Home is brought to the
117      * front while still keeping the currently focused task that is being dragged resumed. This
118      * allows the animation handler to reorder the task to the front and to scale it with the
119      * gesture into the desktop area with the Home and wallpaper behind it.
120      *
121      * Note that the transition handler for this transition doesn't call the finish callback until
122      * after one of the "end" or "cancel" transitions is merged into this transition.
123      */
124     fun startDragToDesktopTransition(
125         taskInfo: RunningTaskInfo,
126         dragToDesktopAnimator: MoveToDesktopAnimator,
127         visualIndicator: DesktopModeVisualIndicator?,
128         dragCancelCallback: Runnable,
129     ) {
130         if (inProgress) {
131             logV("Drag to desktop transition already in progress.")
132             return
133         }
134 
135         val options =
136             ActivityOptions.makeBasic().apply {
137                 setTransientLaunch()
138                 setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
139                 pendingIntentCreatorBackgroundActivityStartMode =
140                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
141             }
142         // If we are launching home for a profile of a user, just use the [userId] of that user
143         // instead of the [profileId] to create the context.
144         val userToLaunchWith =
145             UserHandle.of(desktopUserRepositories.getUserIdForProfile(taskInfo.userId))
146         val pendingIntent =
147             PendingIntent.getActivityAsUser(
148                 context.createContextAsUser(userToLaunchWith, /* flags= */ 0),
149                 /* requestCode= */ 0,
150                 launchHomeIntent,
151                 FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
152                 options.toBundle(),
153                 userToLaunchWith,
154             )
155         val wct = WindowContainerTransaction()
156         // The app that is being dragged into desktop mode might cause new transitions, make this
157         // launch transient to make sure those transitions can execute in parallel and thus won't
158         // block the end-drag transition.
159         val intentOptions = ActivityOptions.makeBasic().setTransientLaunch()
160         wct.sendPendingIntent(pendingIntent, launchHomeIntent, intentOptions.toBundle())
161         val startTransitionToken =
162             transitions.startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
163 
164         transitionState =
165             if (isSplitTask(taskInfo.taskId)) {
166                 val otherTask =
167                     getOtherSplitTask(taskInfo.taskId)
168                         ?: throw IllegalStateException("Expected split task to have a counterpart.")
169                 TransitionState.FromSplit(
170                     draggedTaskId = taskInfo.taskId,
171                     dragAnimator = dragToDesktopAnimator,
172                     startTransitionToken = startTransitionToken,
173                     otherSplitTask = otherTask,
174                     visualIndicator = visualIndicator,
175                     dragCancelCallback = dragCancelCallback,
176                 )
177             } else {
178                 TransitionState.FromFullscreen(
179                     draggedTaskId = taskInfo.taskId,
180                     dragAnimator = dragToDesktopAnimator,
181                     startTransitionToken = startTransitionToken,
182                     visualIndicator = visualIndicator,
183                     dragCancelCallback = dragCancelCallback,
184                 )
185             }
186     }
187 
188     /**
189      * Starts a transition that "finishes" the drag to desktop gesture. This transition is intended
190      * to merge into the "start" transition and is the one that actually applies the bounds and
191      * windowing mode changes to the dragged task. This is called when the dragged task is released
192      * inside the desktop drop zone.
193      */
194     fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? {
195         if (!inProgress) {
196             logV("finishDragToDesktop: not in progress, returning")
197             // Don't attempt to finish a drag to desktop transition since there is no transition in
198             // progress which means that the drag to desktop transition was never successfully
199             // started.
200             return null
201         }
202         val state = requireTransitionState()
203         if (state.startAborted) {
204             logV("finishDragToDesktop: start was aborted, clearing state")
205             // Don't attempt to complete the drag-to-desktop since the start transition didn't
206             // succeed as expected. Just reset the state as if nothing happened.
207             clearState()
208             return null
209         }
210         if (state.startInterrupted) {
211             logV("finishDragToDesktop: start was interrupted, returning")
212             // If start was interrupted we've either already requested a cancel/end transition - so
213             // we should let that request play out, or we're cancelling the drag-to-desktop
214             // transition altogether, so just return here.
215             return null
216         }
217         state.endTransitionToken =
218             transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
219         return state.endTransitionToken
220     }
221 
222     /**
223      * Starts a transition that "cancels" the drag to desktop gesture. This transition is intended
224      * to merge into the "start" transition and it restores the transient state that was used to
225      * launch the Home task over the dragged task. This is called when the dragged task is released
226      * outside the desktop drop zone and is instead dropped back into the status bar region that
227      * means the user wants to remain in their current windowing mode.
228      */
229     fun cancelDragToDesktopTransition(cancelState: CancelState) {
230         if (!inProgress) {
231             logV("cancelDragToDesktop: not in progress, returning")
232             // Don't attempt to cancel a drag to desktop transition since there is no transition in
233             // progress which means that the drag to desktop transition was never successfully
234             // started.
235             return
236         }
237         val state = requireTransitionState()
238         if (state.startAborted) {
239             logV("cancelDragToDesktop: start was aborted, clearing state")
240             // Don't attempt to cancel the drag-to-desktop since the start transition didn't
241             // succeed as expected. Just reset the state as if nothing happened.
242             clearState()
243             return
244         }
245         if (state.startInterrupted) {
246             logV("cancelDragToDesktop: start was interrupted, returning")
247             // If start was interrupted we've either already requested a cancel/end transition - so
248             // we should let that request play out, or we're cancelling the drag-to-desktop
249             // transition altogether, so just return here.
250             return
251         }
252         state.cancelState = cancelState
253 
254         if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
255             // Regular case, transient launch of Home happened as is waiting for the cancel
256             // transient to start and merge. Animate the cancellation (scale back to original
257             // bounds) first before actually starting the cancel transition so that the wallpaper
258             // is visible behind the animating task.
259             state.activeCancelAnimation = startCancelAnimation()
260         } else if (
261             state.draggedTaskChange != null &&
262                 (cancelState == CancelState.CANCEL_SPLIT_LEFT ||
263                     cancelState == CancelState.CANCEL_SPLIT_RIGHT)
264         ) {
265             // We have a valid dragged task, but the animation will be handled by
266             // SplitScreenController; request the transition here.
267             @SplitPosition
268             val splitPosition =
269                 if (cancelState == CancelState.CANCEL_SPLIT_LEFT) {
270                     SPLIT_POSITION_TOP_OR_LEFT
271                 } else {
272                     SPLIT_POSITION_BOTTOM_OR_RIGHT
273                 }
274             val wct = WindowContainerTransaction()
275             restoreWindowOrder(wct, state)
276             state.startTransitionFinishTransaction?.apply()
277             state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
278             requestSplitFromScaledTask(splitPosition, wct)
279             clearState()
280         } else if (
281             state.draggedTaskChange != null &&
282                 (cancelState == CancelState.CANCEL_BUBBLE_LEFT ||
283                     cancelState == CancelState.CANCEL_BUBBLE_RIGHT)
284         ) {
285             if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
286                 // TODO(b/388853233): add support for dragging split task to bubble
287                 state.activeCancelAnimation = startCancelAnimation()
288             } else {
289                 // Animation is handled by BubbleController
290                 val wct = WindowContainerTransaction()
291                 restoreWindowOrder(wct, state)
292                 val onLeft = cancelState == CancelState.CANCEL_BUBBLE_LEFT
293                 requestBubbleFromScaledTask(wct, onLeft)
294             }
295         } else {
296             // There's no dragged task, this can happen when the "cancel" happened too quickly
297             // before the "start" transition is even ready (like on a fling gesture). The
298             // "shrink" animation didn't even start, so there's no need to animate the "cancel".
299             // We also don't want to start the cancel transition yet since we don't have
300             // enough info to restore the order. We'll check for the cancelled state flag when
301             // the "start" animation is ready and cancel from #startAnimation instead.
302         }
303     }
304 
305     /** Calculate the bounds of a scaled task, then use those bounds to request split select. */
306     private fun requestSplitFromScaledTask(
307         @SplitPosition splitPosition: Int,
308         wct: WindowContainerTransaction,
309     ) {
310         val state = requireTransitionState()
311         val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo")
312         val animatedTaskBounds = getAnimatedTaskBounds()
313         state.dragAnimator.cancelAnimator()
314         requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds)
315     }
316 
317     private fun getAnimatedTaskBounds(): Rect {
318         val state = requireTransitionState()
319         val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo")
320         val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds)
321         val taskScale = state.dragAnimator.scale
322         val scaledWidth = taskBounds.width() * taskScale
323         val scaledHeight = taskBounds.height() * taskScale
324         val dragPosition = PointF(state.dragAnimator.position)
325         return Rect(
326             dragPosition.x.toInt(),
327             dragPosition.y.toInt(),
328             (dragPosition.x + scaledWidth).toInt(),
329             (dragPosition.y + scaledHeight).toInt(),
330         )
331     }
332 
333     private fun requestSplitSelect(
334         wct: WindowContainerTransaction,
335         taskInfo: RunningTaskInfo,
336         @SplitPosition splitPosition: Int,
337         taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds),
338     ) {
339         // Prepare to exit split in order to enter split select.
340         if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
341             splitScreenController.prepareExitSplitScreen(
342                 wct,
343                 splitScreenController.getStageOfTask(taskInfo.taskId),
344                 SplitScreenController.EXIT_REASON_DESKTOP_MODE,
345             )
346             splitScreenController.transitionHandler.onSplitToDesktop()
347         }
348         wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
349         wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi)
350         splitScreenController.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds)
351     }
352 
353     private fun requestBubbleFromScaledTask(wct: WindowContainerTransaction, onLeft: Boolean) {
354         // TODO(b/391928049): update density once we can drag from desktop to bubble
355         val state = requireTransitionState()
356         val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo")
357         val dragPosition = PointF(state.dragAnimator.position)
358         val scale = state.dragAnimator.scale
359         val cornerRadius = state.dragAnimator.cornerRadius
360         state.dragAnimator.cancelAnimator()
361         requestBubble(wct, taskInfo, onLeft, scale, cornerRadius, dragPosition)
362     }
363 
364     private fun requestBubble(
365         wct: WindowContainerTransaction,
366         taskInfo: RunningTaskInfo,
367         onLeft: Boolean,
368         taskScale: Float = 1f,
369         cornerRadius: Float = 0f,
370         dragPosition: PointF = PointF(0f, 0f),
371     ) {
372         val controller =
373             bubbleController.orElseThrow { IllegalStateException("BubbleController not set") }
374         controller.expandStackAndSelectBubble(
375             taskInfo,
376             BubbleTransitions.DragData(onLeft, taskScale, cornerRadius, dragPosition, wct),
377         )
378     }
379 
380     override fun startAnimation(
381         transition: IBinder,
382         info: TransitionInfo,
383         startTransaction: SurfaceControl.Transaction,
384         finishTransaction: SurfaceControl.Transaction,
385         finishCallback: Transitions.TransitionFinishCallback,
386     ): Boolean {
387         val state = requireTransitionState()
388 
389         if (
390             handleCancelOrExitAfterInterrupt(
391                 transition,
392                 info,
393                 startTransaction,
394                 finishTransaction,
395                 finishCallback,
396                 state,
397             )
398         ) {
399             return true
400         }
401 
402         val isStartDragToDesktop =
403             info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
404                 transition == state.startTransitionToken
405         if (!isStartDragToDesktop) {
406             return false
407         }
408 
409         val layers = calculateStartDragToDesktopLayers(info)
410         val leafTaskFilter = TransitionUtil.LeafTaskFilter()
411         info.changes.withIndex().forEach { (i, change) ->
412             if (TransitionUtil.isWallpaper(change)) {
413                 val layer = layers.topWallpaperLayer - i
414                 startTransaction.apply {
415                     setLayer(change.leash, layer)
416                     show(change.leash)
417                 }
418             } else if (isHomeChange(change)) {
419                 state.homeChange = change
420                 val layer = layers.topHomeLayer - i
421                 startTransaction.apply {
422                     setLayer(change.leash, layer)
423                     show(change.leash)
424                 }
425             } else if (TransitionInfo.isIndependent(change, info)) {
426                 // Root(s).
427                 when (state) {
428                     is TransitionState.FromSplit -> {
429                         state.splitRootChange = change
430                         val layer =
431                             if (state.cancelState == CancelState.NO_CANCEL) {
432                                 // Normal case, split root goes to the bottom behind everything
433                                 // else.
434                                 layers.topAppLayer - i
435                             } else {
436                                 // Cancel-early case, pretend nothing happened so split root stays
437                                 // top.
438                                 layers.dragLayer
439                             }
440                         startTransaction.apply {
441                             setLayer(change.leash, layer)
442                             show(change.leash)
443                         }
444                     }
445                     is TransitionState.FromFullscreen -> {
446                         // Most of the time we expect one change/task here, which should be the
447                         // same that initiated the drag and that should be layered on top of
448                         // everything.
449                         if (change.taskInfo?.taskId == state.draggedTaskId) {
450                             state.draggedTaskChange = change
451                             val bounds = change.endAbsBounds
452                             startTransaction.apply {
453                                 setLayer(change.leash, layers.dragLayer)
454                                 setWindowCrop(change.leash, bounds.width(), bounds.height())
455                                 show(change.leash)
456                             }
457                         } else {
458                             // It's possible to see an additional change that isn't the dragged
459                             // task when the dragged task is translucent and so the task behind it
460                             // is included in the transition since it was visible and is now being
461                             // occluded by the Home task. Just layer it at the bottom and save it
462                             // in case we need to restore order if the drag is cancelled.
463                             state.otherRootChanges.add(change)
464                             val bounds = change.endAbsBounds
465                             startTransaction.apply {
466                                 setLayer(change.leash, layers.topAppLayer - i)
467                                 setWindowCrop(change.leash, bounds.width(), bounds.height())
468                                 show(change.leash)
469                             }
470                         }
471                     }
472                 }
473             } else if (leafTaskFilter.test(change)) {
474                 // When dragging one of the split tasks, the dragged leaf needs to be re-parented
475                 // so that it can be layered separately from the rest of the split root/stages.
476                 // The split root including the other split side was layered behind the wallpaper
477                 // and home while the dragged split needs to be layered in front of them.
478                 // Do not do this in the cancel-early case though, since in that case nothing should
479                 // happen on screen so the layering will remain the same as if no transition
480                 // occurred.
481                 if (
482                     change.taskInfo?.taskId == state.draggedTaskId &&
483                         state.cancelState != CancelState.STANDARD_CANCEL
484                 ) {
485                     // We need access to the dragged task's change in both non-cancel and split
486                     // cancel cases.
487                     state.draggedTaskChange = change
488                 }
489                 if (
490                     change.taskInfo?.taskId == state.draggedTaskId &&
491                         state.cancelState == CancelState.NO_CANCEL
492                 ) {
493                     taskDisplayAreaOrganizer.reparentToDisplayArea(
494                         change.endDisplayId,
495                         change.leash,
496                         startTransaction,
497                     )
498                     val bounds = change.endAbsBounds
499                     startTransaction.apply {
500                         setLayer(change.leash, layers.dragLayer)
501                         setWindowCrop(change.leash, bounds.width(), bounds.height())
502                         show(change.leash)
503                     }
504                 }
505             }
506         }
507         state.surfaceLayers = layers
508         state.startTransitionFinishCb = finishCallback
509         state.startTransitionFinishTransaction = finishTransaction
510 
511         val taskChange = state.draggedTaskChange ?: error("Expected non-null task change.")
512         val taskInfo = taskChange.taskInfo ?: error("Expected non-null task info.")
513 
514         if (DesktopModeFlags.ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX.isTrue) {
515             attachIndicatorToTransitionRoot(state, info, taskInfo, startTransaction)
516         }
517         startTransaction.apply()
518 
519         if (state.cancelState == CancelState.NO_CANCEL) {
520             // Normal case, start animation to scale down the dragged task. It'll also be moved to
521             // follow the finger and when released we'll start the next phase/transition.
522             state.dragAnimator.startAnimation()
523         } else if (state.cancelState == CancelState.STANDARD_CANCEL) {
524             // Cancel-early case, the state was flagged was cancelled already, which means the
525             // gesture ended in the cancel region. This can happen even before the start transition
526             // is ready/animate here when cancelling quickly like with a fling. There's no point
527             // in starting the scale down animation that we would scale up anyway, so just jump
528             // directly into starting the cancel transition to restore WM order. Surfaces should
529             // not move as if no transition happened.
530             startCancelDragToDesktopTransition()
531         } else if (
532             state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
533                 state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
534         ) {
535             // Cancel-early case for split-cancel. The state was flagged already as a cancel for
536             // requesting split select. Similar to the above, this can happen due to quick fling
537             // gestures. We can simply request split here without needing to calculate animated
538             // task bounds as the task has not shrunk at all.
539             val splitPosition =
540                 if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) {
541                     SPLIT_POSITION_TOP_OR_LEFT
542                 } else {
543                     SPLIT_POSITION_BOTTOM_OR_RIGHT
544                 }
545             val wct = WindowContainerTransaction()
546             restoreWindowOrder(wct)
547             state.startTransitionFinishTransaction?.apply()
548             state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
549             requestSplitSelect(wct, taskInfo, splitPosition)
550         } else if (
551             state.cancelState == CancelState.CANCEL_BUBBLE_LEFT ||
552                 state.cancelState == CancelState.CANCEL_BUBBLE_RIGHT
553         ) {
554             if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
555                 // TODO(b/388853233): add support for dragging split task to bubble
556                 startCancelDragToDesktopTransition()
557                 return true
558             }
559             val taskInfo =
560                 state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.")
561             val wct = WindowContainerTransaction()
562             restoreWindowOrder(wct)
563             val onLeft = state.cancelState == CancelState.CANCEL_BUBBLE_LEFT
564             requestBubble(wct, taskInfo, onLeft)
565         }
566         return true
567     }
568 
569     private fun attachIndicatorToTransitionRoot(
570         state: TransitionState,
571         info: TransitionInfo,
572         taskInfo: RunningTaskInfo,
573         t: SurfaceControl.Transaction,
574     ) {
575         val transitionRoot = info.getRoot(info.findRootIndex(taskInfo.displayId))
576         state.visualIndicator?.let {
577             // Attach the indicator to the transition root so that it's removed at the end of the
578             // transition regardless of whether we managed to release the indicator.
579             it.reparentLeash(t, transitionRoot.leash)
580             it.fadeInIndicator()
581         }
582     }
583 
584     private fun handleCancelOrExitAfterInterrupt(
585         transition: IBinder,
586         info: TransitionInfo,
587         startTransaction: Transaction,
588         finishTransaction: Transaction,
589         finishCallback: Transitions.TransitionFinishCallback,
590         state: TransitionState,
591     ): Boolean {
592         if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
593             return false
594         }
595         val isCancelDragToDesktop =
596             info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
597                 transition == state.cancelTransitionToken
598         val isEndDragToDesktop =
599             info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
600                 transition == state.endTransitionToken
601         // We should only receive cancel or end transitions through startAnimation() if the
602         // start transition was interrupted while a cancel- or end-transition had already
603         // been requested. Finish the cancel/end transition to avoid having to deal with more
604         // incoming transitions, and clear the state for the next start-drag transition.
605         if (!isCancelDragToDesktop && !isEndDragToDesktop) {
606             return false
607         }
608         if (!state.startInterrupted) {
609             logW(
610                 "Not interrupted, but received startAnimation for cancel/end drag." +
611                     "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
612             )
613             return false
614         }
615         logV(
616             "startAnimation: interrupted -> " +
617                 "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
618         )
619         if (isEndDragToDesktop) {
620             setupEndDragToDesktop(info, startTransaction, finishTransaction)
621             animateEndDragToDesktop(startTransaction = startTransaction, finishCallback)
622         } else { // isCancelDragToDesktop
623             // Similar to when we merge the cancel transition: ensure all tasks involved in the
624             // cancel transition are shown, and finish the transition immediately.
625             info.changes.forEach { change ->
626                 startTransaction.show(change.leash)
627                 finishTransaction.show(change.leash)
628             }
629         }
630         startTransaction.apply()
631         finishCallback.onTransitionFinished(/* wct= */ null)
632         clearState()
633         return true
634     }
635 
636     /**
637      * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated
638      * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is
639      * the change index.
640      */
641     protected abstract fun calculateStartDragToDesktopLayers(
642         info: TransitionInfo
643     ): DragToDesktopLayers
644 
645     override fun mergeAnimation(
646         transition: IBinder,
647         info: TransitionInfo,
648         startT: SurfaceControl.Transaction,
649         finishT: SurfaceControl.Transaction,
650         mergeTarget: IBinder,
651         finishCallback: Transitions.TransitionFinishCallback,
652     ) {
653         val state = requireTransitionState()
654         // We don't want to merge the split select animation if that's what we requested.
655         if (
656             state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
657                 state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
658         ) {
659             logV("mergeAnimation: cancel through split")
660             clearState()
661             return
662         }
663         // In case of bubble animation, finish the initial desktop drag animation, but keep the
664         // current animation running and have bubbles take over
665         if (info.type == TRANSIT_CONVERT_TO_BUBBLE) {
666             logV("mergeAnimation: convert-to-bubble")
667             state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
668             clearState()
669             return
670         }
671         val isCancelTransition =
672             info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
673                 transition == state.cancelTransitionToken &&
674                 mergeTarget == state.startTransitionToken
675         val isEndTransition =
676             info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
677                 mergeTarget == state.startTransitionToken
678 
679         val startTransactionFinishT =
680             state.startTransitionFinishTransaction
681                 ?: error("Start transition expected to be waiting for merge but wasn't")
682         val startTransitionFinishCb =
683             state.startTransitionFinishCb
684                 ?: error("Start transition expected to be waiting for merge but wasn't")
685         if (isEndTransition) {
686             logV("mergeAnimation: end-transition, target=$mergeTarget")
687             state.mergedEndTransition = true
688             setupEndDragToDesktop(
689                 info,
690                 startTransaction = startT,
691                 finishTransaction = startTransactionFinishT,
692             )
693             // Call finishCallback to merge animation before startTransitionFinishCb is called
694             finishCallback.onTransitionFinished(/* wct= */ null)
695             LatencyTracker.getInstance(context)
696                 .onActionEnd(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
697             animateEndDragToDesktop(startTransaction = startT, startTransitionFinishCb)
698             return
699         }
700         if (isCancelTransition) {
701             logV("mergeAnimation: cancel-transition, target=$mergeTarget")
702             LatencyTracker.getInstance(context)
703                 .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
704             info.changes.forEach { change ->
705                 startT.show(change.leash)
706                 startTransactionFinishT.show(change.leash)
707             }
708             startT.apply()
709             finishCallback.onTransitionFinished(/* wct= */ null)
710             startTransitionFinishCb.onTransitionFinished(/* wct= */ null)
711             clearState()
712             return
713         }
714         logW("unhandled merge transition: transitionInfo=$info")
715         // Handle unknown incoming transitions by finishing the start transition. For now, only do
716         // this if we've already requested a cancel- or end transition. If we've already merged the
717         // end-transition, or if the end-transition is running on its own, then just wait until that
718         // finishes instead. If we've merged the cancel-transition we've finished the
719         // start-transition and won't reach this code.
720         if (mergeTarget == state.startTransitionToken && !state.mergedEndTransition) {
721             interruptStartTransition(state)
722         }
723     }
724 
725     private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean =
726         state.cancelTransitionToken != null || state.endTransitionToken != null
727 
728     private fun interruptStartTransition(state: TransitionState) {
729         if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
730             return
731         }
732         if (isCancelOrEndTransitionRequested(state)) {
733             logV("interruptStartTransition, bookend requested -> finish start transition")
734             // Finish the start-drag transition, we will finish the overall transition properly when
735             // receiving #startAnimation for Cancel/End.
736             state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
737             state.dragAnimator.cancelAnimator()
738         } else {
739             logV("interruptStartTransition, bookend not requested -> animate to Home")
740             // Animate to Home, and then finish the start-drag transition. Since there is no other
741             // (end/cancel) transition requested that will be the end of the overall transition.
742             state.dragAnimator.cancelAnimator()
743             state.dragCancelCallback?.run()
744             createInterruptToHomeAnimator(transactionSupplier.get(), state) {
745                 state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
746                 clearState()
747             }
748         }
749         state.activeCancelAnimation?.removeAllListeners()
750         state.activeCancelAnimation?.cancel()
751         state.activeCancelAnimation = null
752         // Keep the transition state so we can deal with Cancel/End properly in #startAnimation.
753         state.startInterrupted = true
754         dragToDesktopStateListener?.onTransitionInterrupted()
755         // Cancel CUJs here as they won't be accurate now that an incoming transition is playing.
756         interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
757         interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
758         LatencyTracker.getInstance(context)
759             .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
760     }
761 
762     private fun createInterruptToHomeAnimator(
763         transaction: Transaction,
764         state: TransitionState,
765         endCallback: Runnable,
766     ) {
767         val homeLeash = state.homeChange?.leash ?: error("Expected home leash to be non-null")
768         val draggedTaskLeash =
769             state.draggedTaskChange?.leash ?: error("Expected dragged leash to be non-null")
770         val homeAnimator = createInterruptAlphaAnimator(transaction, homeLeash, toShow = true)
771         val draggedTaskAnimator =
772             createInterruptAlphaAnimator(transaction, draggedTaskLeash, toShow = false)
773         val animatorSet = AnimatorSet()
774         animatorSet.playTogether(homeAnimator, draggedTaskAnimator)
775         animatorSet.addListener(
776             object : AnimatorListenerAdapter() {
777                 override fun onAnimationEnd(animation: Animator) {
778                     endCallback.run()
779                 }
780             }
781         )
782         animatorSet.start()
783     }
784 
785     private fun createInterruptAlphaAnimator(
786         transaction: Transaction,
787         leash: SurfaceControl,
788         toShow: Boolean,
789     ) =
790         ValueAnimator.ofFloat(if (toShow) 0f else 1f, if (toShow) 1f else 0f).apply {
791             transaction.show(leash)
792             duration = DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS
793             interpolator = Interpolators.LINEAR
794             addUpdateListener { animation ->
795                 transaction
796                     .setAlpha(leash, animation.animatedValue as Float)
797                     .setFrameTimeline(Choreographer.getInstance().vsyncId)
798                     .apply()
799             }
800         }
801 
802     protected open fun setupEndDragToDesktop(
803         info: TransitionInfo,
804         startTransaction: SurfaceControl.Transaction,
805         finishTransaction: SurfaceControl.Transaction,
806     ) {
807         val state = requireTransitionState()
808         val freeformTaskChanges = mutableListOf<Change>()
809         info.changes.forEachIndexed { i, change ->
810             when {
811                 state is TransitionState.FromSplit &&
812                     change.taskInfo?.taskId == state.otherSplitTask -> {
813                     // If we're exiting split, hide the remaining split task.
814                     startTransaction.hide(change.leash)
815                     finishTransaction.hide(change.leash)
816                 }
817                 change.mode == TRANSIT_CLOSE -> {
818                     startTransaction.hide(change.leash)
819                     finishTransaction.hide(change.leash)
820                 }
821                 change.taskInfo?.taskId == state.draggedTaskId -> {
822                     startTransaction.show(change.leash)
823                     finishTransaction.show(change.leash)
824                     state.draggedTaskChange = change
825                     // Restoring the dragged leash layer as it gets reset in the merge transition
826                     state.surfaceLayers?.let {
827                         startTransaction.setLayer(change.leash, it.dragLayer)
828                     }
829                 }
830                 change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM -> {
831                     // Other freeform tasks that are being restored go behind the dragged task.
832                     val draggedTaskLeash =
833                         state.draggedTaskChange?.leash
834                             ?: error("Expected dragged leash to be non-null")
835                     startTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i)
836                     finishTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i)
837                     freeformTaskChanges.add(change)
838                 }
839             }
840         }
841 
842         state.freeformTaskChanges = freeformTaskChanges
843     }
844 
845     protected open fun animateEndDragToDesktop(
846         startTransaction: SurfaceControl.Transaction,
847         startTransitionFinishCb: Transitions.TransitionFinishCallback,
848     ) {
849         val state = requireTransitionState()
850         val draggedTaskChange =
851             state.draggedTaskChange ?: error("Expected non-null change of dragged task")
852         val draggedTaskLeash = draggedTaskChange.leash
853         val startBounds = draggedTaskChange.startAbsBounds
854         val endBounds = draggedTaskChange.endAbsBounds
855 
856         // Cancel any animation that may be currently playing; we will use the relevant
857         // details of that animation here.
858         state.dragAnimator.cancelAnimator()
859         // We still apply scale to task bounds; as we animate the bounds to their
860         // end value, animate scale to 1.
861         val startScale = state.dragAnimator.scale
862         val startPosition = state.dragAnimator.position
863         val unscaledStartWidth = startBounds.width()
864         val unscaledStartHeight = startBounds.height()
865         val unscaledStartBounds =
866             Rect(
867                 startPosition.x.toInt(),
868                 startPosition.y.toInt(),
869                 startPosition.x.toInt() + unscaledStartWidth,
870                 startPosition.y.toInt() + unscaledStartHeight,
871             )
872 
873         dragToDesktopStateListener?.onCommitToDesktopAnimationStart()
874         // Accept the merge by applying the merging transaction (applied by #showResizeVeil)
875         // and finish callback. Show the veil and position the task at the first frame before
876         // starting the final animation.
877         onTaskResizeAnimationListener.onAnimationStart(
878             state.draggedTaskId,
879             startTransaction,
880             unscaledStartBounds,
881         )
882         val tx: SurfaceControl.Transaction = transactionSupplier.get()
883         ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds)
884             .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
885             .apply {
886                 addUpdateListener { animator ->
887                     val animBounds = animator.animatedValue as Rect
888                     val animFraction = animator.animatedFraction
889                     // Progress scale from starting value to 1 as animation plays.
890                     val animScale = startScale + animFraction * (1 - startScale)
891                     tx.apply {
892                         setScale(draggedTaskLeash, animScale, animScale)
893                         setPosition(
894                             draggedTaskLeash,
895                             animBounds.left.toFloat(),
896                             animBounds.top.toFloat(),
897                         )
898                         setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height())
899                     }
900                     onTaskResizeAnimationListener.onBoundsChange(
901                         state.draggedTaskId,
902                         tx,
903                         animBounds,
904                     )
905                 }
906                 addListener(
907                     object : AnimatorListenerAdapter() {
908                         override fun onAnimationEnd(animation: Animator) {
909                             onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
910                             startTransitionFinishCb.onTransitionFinished(/* wct= */ null)
911                             clearState()
912                             interactionJankMonitor.end(
913                                 CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
914                             )
915                         }
916                     }
917                 )
918                 start()
919             }
920     }
921 
922     override fun handleRequest(
923         transition: IBinder,
924         request: TransitionRequestInfo,
925     ): WindowContainerTransaction? {
926         // Only handle transitions started from shell.
927         return null
928     }
929 
930     override fun onTransitionConsumed(
931         transition: IBinder,
932         aborted: Boolean,
933         finishTransaction: SurfaceControl.Transaction?,
934     ) {
935         val state = transitionState ?: return
936         if (!aborted) {
937             return
938         }
939         if (state.startTransitionToken == transition) {
940             logV("onTransitionConsumed() start transition aborted")
941             state.startAborted = true
942             // The start-transition (DRAG_HOLD) is aborted, cancel its jank interaction.
943             interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
944         } else if (state.cancelTransitionToken == transition) {
945             state.draggedTaskChange?.leash?.let { state.startTransitionFinishTransaction?.show(it) }
946             state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
947             clearState()
948         } else {
949             // This transition being aborted is neither the start, nor the cancel transition, so
950             // it must be the finish transition (DRAG_RELEASE); cancel its jank interaction.
951             interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
952         }
953     }
954 
955     /** Checks if the change is a home task change */
956     @VisibleForTesting
957     fun isHomeChange(change: Change): Boolean {
958         return change.taskInfo?.let {
959             it.activityType == ACTIVITY_TYPE_HOME &&
960                 // Skip translucent wizard task with type home
961                 // TODO(b/368334295): Remove when the multiple home changes issue is resolved
962                 !(it.isTopActivityTransparent && it.numActivities == 1)
963         } ?: false
964     }
965 
966     private fun startCancelAnimation(): Animator {
967         val state = requireTransitionState()
968         val dragToDesktopAnimator = state.dragAnimator
969 
970         val draggedTaskChange =
971             state.draggedTaskChange ?: throw IllegalStateException("Expected non-null task change")
972         val sc = draggedTaskChange.leash
973         // Pause the animation that shrinks the window when task is first dragged from fullscreen
974         dragToDesktopAnimator.cancelAnimator()
975         // Then animate the scaled window back to its original bounds.
976         val x: Float = dragToDesktopAnimator.position.x
977         val y: Float = dragToDesktopAnimator.position.y
978         val targetX = draggedTaskChange.endAbsBounds.left
979         val targetY = draggedTaskChange.endAbsBounds.top
980         val dx = targetX - x
981         val dy = targetY - y
982         val tx: SurfaceControl.Transaction = transactionSupplier.get()
983         return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
984             .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
985             .apply {
986                 addUpdateListener { animator ->
987                     val scale = animator.animatedValue as Float
988                     val fraction = animator.animatedFraction
989                     val animX = x + (dx * fraction)
990                     val animY = y + (dy * fraction)
991                     tx.apply {
992                         setPosition(sc, animX, animY)
993                         setScale(sc, scale, scale)
994                         show(sc)
995                         apply()
996                     }
997                 }
998                 addListener(
999                     object : AnimatorListenerAdapter() {
1000                         override fun onAnimationEnd(animation: Animator) {
1001                             state.activeCancelAnimation = null
1002                             dragToDesktopStateListener?.onCancelToDesktopAnimationEnd()
1003                             // Start the cancel transition to restore order.
1004                             startCancelDragToDesktopTransition()
1005                         }
1006                     }
1007                 )
1008                 start()
1009             }
1010     }
1011 
1012     private fun startCancelDragToDesktopTransition() {
1013         val state = requireTransitionState()
1014         val wct = WindowContainerTransaction()
1015         restoreWindowOrder(wct, state)
1016         state.cancelTransitionToken =
1017             transitions.startTransition(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this)
1018     }
1019 
1020     private fun restoreWindowOrder(
1021         wct: WindowContainerTransaction,
1022         state: TransitionState = requireTransitionState(),
1023     ) {
1024         when (state) {
1025             is TransitionState.FromFullscreen -> {
1026                 // There may have been tasks sent behind home that are not the dragged task (like
1027                 // when the dragged task is translucent and that makes the task behind it visible).
1028                 // Restore the order of those first.
1029                 state.otherRootChanges
1030                     .mapNotNull { it.container }
1031                     .forEach { wc ->
1032                         // TODO(b/322852244): investigate why even though these "other" tasks are
1033                         //  reordered in front of home and behind the translucent dragged task, its
1034                         //  surface is not visible on screen.
1035                         wct.reorder(wc, /* onTop= */ true)
1036                     }
1037                 val wc =
1038                     state.draggedTaskChange?.container
1039                         ?: error("Dragged task should be non-null before cancelling")
1040                 // Then the dragged task a the very top.
1041                 wct.reorder(wc, /* onTop= */ true)
1042             }
1043             is TransitionState.FromSplit -> {
1044                 val wc =
1045                     state.splitRootChange?.container
1046                         ?: error("Split root should be non-null before cancelling")
1047                 wct.reorder(wc, /* onTop= */ true)
1048             }
1049         }
1050         val homeWc =
1051             state.homeChange?.container ?: error("Home task should be non-null before cancelling")
1052         wct.restoreTransientOrder(homeWc)
1053     }
1054 
1055     protected fun clearState() {
1056         transitionState = null
1057     }
1058 
1059     private fun isSplitTask(taskId: Int): Boolean =
1060         splitScreenController.isTaskInSplitScreen(taskId)
1061 
1062     private fun getOtherSplitTask(taskId: Int): Int? {
1063         val splitPos = splitScreenController.getSplitPosition(taskId)
1064         if (splitPos == SPLIT_POSITION_UNDEFINED) return null
1065         val otherTaskPos =
1066             if (splitPos == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
1067                 SPLIT_POSITION_TOP_OR_LEFT
1068             } else {
1069                 SPLIT_POSITION_BOTTOM_OR_RIGHT
1070             }
1071         return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
1072     }
1073 
1074     protected fun requireTransitionState(): TransitionState =
1075         transitionState ?: error("Expected non-null transition state")
1076 
1077     /**
1078      * Represents the layering (Z order) that will be given to any window based on its type during
1079      * the "start" transition of the drag-to-desktop transition.
1080      *
1081      * @param topAppLayer Used to calculate the app layer z-order = `topAppLayer - changeIndex`.
1082      * @param topHomeLayer Used to calculate the home layer z-order = `topHomeLayer - changeIndex`.
1083      * @param topWallpaperLayer Used to calculate the wallpaper layer z-order = `topWallpaperLayer -
1084      *   changeIndex`
1085      * @param dragLayer Defines the drag layer z-order
1086      */
1087     data class DragToDesktopLayers(
1088         val topAppLayer: Int,
1089         val topHomeLayer: Int,
1090         val topWallpaperLayer: Int,
1091         val dragLayer: Int,
1092     )
1093 
1094     /** Listener for various events happening during the DragToDesktop transition. */
1095     interface DragToDesktopStateListener {
1096         /** Indicates that the animation into Desktop has started. */
1097         fun onCommitToDesktopAnimationStart()
1098 
1099         /** Called when the animation to cancel the desktop-drag has finished. */
1100         fun onCancelToDesktopAnimationEnd()
1101 
1102         /** Indicates that the drag-to-desktop transition has been interrupted. */
1103         fun onTransitionInterrupted()
1104     }
1105 
1106     sealed class TransitionState {
1107         abstract val draggedTaskId: Int
1108         abstract val dragAnimator: MoveToDesktopAnimator
1109         abstract val startTransitionToken: IBinder
1110         abstract var startTransitionFinishCb: Transitions.TransitionFinishCallback?
1111         abstract var startTransitionFinishTransaction: SurfaceControl.Transaction?
1112         abstract var cancelTransitionToken: IBinder?
1113         abstract var homeChange: Change?
1114         abstract var draggedTaskChange: Change?
1115         abstract var freeformTaskChanges: List<Change>
1116         abstract var surfaceLayers: DragToDesktopLayers?
1117         abstract var cancelState: CancelState
1118         abstract var startAborted: Boolean
1119         abstract val visualIndicator: DesktopModeVisualIndicator?
1120         abstract var startInterrupted: Boolean
1121         abstract var endTransitionToken: IBinder?
1122         abstract var mergedEndTransition: Boolean
1123         abstract var activeCancelAnimation: Animator?
1124         abstract var dragCancelCallback: Runnable?
1125 
1126         data class FromFullscreen(
1127             override val draggedTaskId: Int,
1128             override val dragAnimator: MoveToDesktopAnimator,
1129             override val startTransitionToken: IBinder,
1130             override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
1131             override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
1132             override var cancelTransitionToken: IBinder? = null,
1133             override var homeChange: Change? = null,
1134             override var draggedTaskChange: Change? = null,
1135             override var freeformTaskChanges: List<Change> = emptyList(),
1136             override var surfaceLayers: DragToDesktopLayers? = null,
1137             override var cancelState: CancelState = CancelState.NO_CANCEL,
1138             override var startAborted: Boolean = false,
1139             override val visualIndicator: DesktopModeVisualIndicator?,
1140             override var startInterrupted: Boolean = false,
1141             override var endTransitionToken: IBinder? = null,
1142             override var mergedEndTransition: Boolean = false,
1143             override var activeCancelAnimation: Animator? = null,
1144             override var dragCancelCallback: Runnable? = null,
1145             var otherRootChanges: MutableList<Change> = mutableListOf(),
1146         ) : TransitionState()
1147 
1148         data class FromSplit(
1149             override val draggedTaskId: Int,
1150             override val dragAnimator: MoveToDesktopAnimator,
1151             override val startTransitionToken: IBinder,
1152             override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
1153             override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
1154             override var cancelTransitionToken: IBinder? = null,
1155             override var homeChange: Change? = null,
1156             override var draggedTaskChange: Change? = null,
1157             override var freeformTaskChanges: List<Change> = emptyList(),
1158             override var surfaceLayers: DragToDesktopLayers? = null,
1159             override var cancelState: CancelState = CancelState.NO_CANCEL,
1160             override var startAborted: Boolean = false,
1161             override val visualIndicator: DesktopModeVisualIndicator?,
1162             override var startInterrupted: Boolean = false,
1163             override var endTransitionToken: IBinder? = null,
1164             override var mergedEndTransition: Boolean = false,
1165             override var activeCancelAnimation: Animator? = null,
1166             override var dragCancelCallback: Runnable? = null,
1167             var splitRootChange: Change? = null,
1168             var otherSplitTask: Int,
1169         ) : TransitionState()
1170     }
1171 
1172     /** Enum to provide context on cancelling a drag to desktop event. */
1173     enum class CancelState {
1174         /** No cancel case; this drag is not flagged for a cancel event. */
1175         NO_CANCEL,
1176         /** A standard cancel event; should restore task to previous windowing mode. */
1177         STANDARD_CANCEL,
1178         /** A cancel event where the task will request to enter split on the left side. */
1179         CANCEL_SPLIT_LEFT,
1180         /** A cancel event where the task will request to enter split on the right side. */
1181         CANCEL_SPLIT_RIGHT,
1182         /** A cancel event where the task will request to bubble on the left side. */
1183         CANCEL_BUBBLE_LEFT,
1184         /** A cancel event where the task will request to bubble on the right side. */
1185         CANCEL_BUBBLE_RIGHT,
1186     }
1187 
1188     private fun logV(msg: String, vararg arguments: Any?) {
1189         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1190     }
1191 
1192     private fun logW(msg: String, vararg arguments: Any?) {
1193         ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1194     }
1195 
1196     companion object {
1197         private const val TAG = "DragToDesktopTransitionHandler"
1198         /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
1199         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
1200         const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
1201     }
1202 }
1203 
1204 /** Enables flagged rollout of the [SpringDragToDesktopTransitionHandler] */
1205 class DefaultDragToDesktopTransitionHandler
1206 @JvmOverloads
1207 constructor(
1208     context: Context,
1209     transitions: Transitions,
1210     taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
1211     desktopUserRepositories: DesktopUserRepositories,
1212     interactionJankMonitor: InteractionJankMonitor,
1213     bubbleController: Optional<BubbleController>,
<lambda>null1214     transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier {
1215         SurfaceControl.Transaction()
1216     },
1217 ) :
1218     DragToDesktopTransitionHandler(
1219         context,
1220         transitions,
1221         taskDisplayAreaOrganizer,
1222         desktopUserRepositories,
1223         interactionJankMonitor,
1224         bubbleController,
1225         transactionSupplier,
1226     ) {
1227 
1228     /**
1229      * @return layers in order:
1230      * - appLayers - non-wallpaper, non-home tasks excluding the dragged task go at the bottom
1231      * - homeLayers - home task on top of apps
1232      * - wallpaperLayers - wallpaper on top of home
1233      * - dragLayer - the dragged task on top of everything, there's only 1 dragged task
1234      */
calculateStartDragToDesktopLayersnull1235     override fun calculateStartDragToDesktopLayers(info: TransitionInfo): DragToDesktopLayers =
1236         DragToDesktopLayers(
1237             topAppLayer = info.changes.size,
1238             topHomeLayer = info.changes.size * 2,
1239             topWallpaperLayer = info.changes.size * 3,
1240             dragLayer = info.changes.size * 3,
1241         )
1242 }
1243 
1244 /** Desktop transition handler with spring based animation for the end drag to desktop transition */
1245 class SpringDragToDesktopTransitionHandler
1246 @JvmOverloads
1247 constructor(
1248     context: Context,
1249     transitions: Transitions,
1250     taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
1251     desktopUserRepositories: DesktopUserRepositories,
1252     interactionJankMonitor: InteractionJankMonitor,
1253     bubbleController: Optional<BubbleController>,
1254     transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier {
1255         SurfaceControl.Transaction()
1256     },
1257 ) :
1258     DragToDesktopTransitionHandler(
1259         context,
1260         transitions,
1261         taskDisplayAreaOrganizer,
1262         desktopUserRepositories,
1263         interactionJankMonitor,
1264         bubbleController,
1265         transactionSupplier,
1266     ) {
1267 
1268     private val positionSpringConfig =
1269         PhysicsAnimator.SpringConfig(POSITION_SPRING_STIFFNESS, POSITION_SPRING_DAMPING_RATIO)
1270 
1271     private val sizeSpringConfig =
1272         PhysicsAnimator.SpringConfig(SIZE_SPRING_STIFFNESS, SIZE_SPRING_DAMPING_RATIO)
1273 
1274     /**
1275      * @return layers in order:
1276      * - appLayers - below everything z < 0, effectively hides the leash
1277      * - homeLayers - home task on top of apps, z in 0..<size
1278      * - wallpaperLayers - wallpaper on top of home, z in size..<size*2
1279      * - dragLayer - the dragged task on top of everything, z == size*2
1280      */
calculateStartDragToDesktopLayersnull1281     override fun calculateStartDragToDesktopLayers(info: TransitionInfo): DragToDesktopLayers =
1282         DragToDesktopLayers(
1283             topAppLayer = -1,
1284             topHomeLayer = info.changes.size - 1,
1285             topWallpaperLayer = info.changes.size * 2 - 1,
1286             dragLayer = info.changes.size * 2,
1287         )
1288 
1289     override fun setupEndDragToDesktop(
1290         info: TransitionInfo,
1291         startTransaction: SurfaceControl.Transaction,
1292         finishTransaction: SurfaceControl.Transaction,
1293     ) {
1294         super.setupEndDragToDesktop(info, startTransaction, finishTransaction)
1295 
1296         val state = requireTransitionState()
1297         val homeLeash = state.homeChange?.leash
1298         if (homeLeash == null) {
1299             logE("home leash is null")
1300         } else {
1301             // Hide home on finish to prevent flickering when wallpaper activity flag is enabled
1302             finishTransaction.hide(homeLeash)
1303         }
1304         // Setup freeform tasks before animation
1305         state.freeformTaskChanges.forEach { change ->
1306             val startScale = FREEFORM_TASKS_INITIAL_SCALE
1307             val startX =
1308                 change.endAbsBounds.left + change.endAbsBounds.width() * (1 - startScale) / 2
1309             val startY =
1310                 change.endAbsBounds.top + change.endAbsBounds.height() * (1 - startScale) / 2
1311             startTransaction.setPosition(change.leash, startX, startY)
1312             startTransaction.setScale(change.leash, startScale, startScale)
1313             startTransaction.setAlpha(change.leash, 0f)
1314         }
1315     }
1316 
animateEndDragToDesktopnull1317     override fun animateEndDragToDesktop(
1318         startTransaction: SurfaceControl.Transaction,
1319         startTransitionFinishCb: Transitions.TransitionFinishCallback,
1320     ) {
1321         val state = requireTransitionState()
1322         val draggedTaskChange =
1323             state.draggedTaskChange ?: error("Expected non-null change of dragged task")
1324         val draggedTaskLeash = draggedTaskChange.leash
1325         val freeformTaskChanges = state.freeformTaskChanges
1326         val startBounds = draggedTaskChange.startAbsBounds
1327         val endBounds = draggedTaskChange.endAbsBounds
1328         val currentVelocity = state.dragAnimator.computeCurrentVelocity()
1329 
1330         // Cancel any animation that may be currently playing; we will use the relevant
1331         // details of that animation here.
1332         state.dragAnimator.cancelAnimator()
1333         // We still apply scale to task bounds; as we animate the bounds to their
1334         // end value, animate scale to 1.
1335         val startScale = state.dragAnimator.scale
1336         val startPosition = state.dragAnimator.position
1337         val startBoundsWithOffset =
1338             Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) }
1339 
1340         logV(
1341             "animateEndDragToDesktop: startBounds=$startBounds, endBounds=$endBounds, " +
1342                 "startScale=$startScale, startPosition=$startPosition, " +
1343                 "startBoundsWithOffset=$startBoundsWithOffset"
1344         )
1345 
1346         dragToDesktopStateListener?.onCommitToDesktopAnimationStart()
1347         // Accept the merge by applying the merging transaction (applied by #showResizeVeil)
1348         // and finish callback. Show the veil and position the task at the first frame before
1349         // starting the final animation.
1350         onTaskResizeAnimationListener.onAnimationStart(
1351             state.draggedTaskId,
1352             startTransaction,
1353             startBoundsWithOffset,
1354         )
1355 
1356         val tx: SurfaceControl.Transaction = transactionSupplier.get()
1357         PhysicsAnimator.getInstance(startBoundsWithOffset)
1358             .spring(
1359                 FloatProperties.RECT_X,
1360                 endBounds.left.toFloat(),
1361                 currentVelocity.x,
1362                 positionSpringConfig,
1363             )
1364             .spring(
1365                 FloatProperties.RECT_Y,
1366                 endBounds.top.toFloat(),
1367                 currentVelocity.y,
1368                 positionSpringConfig,
1369             )
1370             .spring(FloatProperties.RECT_WIDTH, endBounds.width().toFloat(), sizeSpringConfig)
1371             .spring(FloatProperties.RECT_HEIGHT, endBounds.height().toFloat(), sizeSpringConfig)
1372             .addUpdateListener { animBounds, _ ->
1373                 val animFraction =
1374                     getAnimationFraction(
1375                         startBounds = startBounds,
1376                         endBounds = endBounds,
1377                         animBounds = animBounds,
1378                     )
1379                 val animScale = startScale + animFraction * (1 - startScale)
1380                 // Freeform animation starts with freeform animation offset relative to the commit
1381                 // animation and plays until the commit animation ends. For instance:
1382                 // - if the freeform animation offset is `0.0` the freeform tasks animate alongside
1383                 // - if the freeform animation offset is `0.6` the freeform tasks will
1384                 //   start animating at 60% fraction of the commit animation and will complete when
1385                 //   the commit animation fraction is 100%.
1386                 // - if the freeform animation offset is `1.0` then freeform tasks will appear
1387                 //   without animation after commit animation finishes.
1388                 val freeformAnimFraction =
1389                     if (FREEFORM_TASKS_ANIM_OFFSET != 1f) {
1390                         max(animFraction - FREEFORM_TASKS_ANIM_OFFSET, 0f) /
1391                             (1f - FREEFORM_TASKS_ANIM_OFFSET)
1392                     } else {
1393                         0f
1394                     }
1395                 val freeformStartScale = FREEFORM_TASKS_INITIAL_SCALE
1396                 val freeformAnimScale =
1397                     freeformStartScale + freeformAnimFraction * (1 - freeformStartScale)
1398                 tx.apply {
1399                     // Update dragged task
1400                     setScale(draggedTaskLeash, animScale, animScale)
1401                     setPosition(
1402                         draggedTaskLeash,
1403                         animBounds.left.toFloat(),
1404                         animBounds.top.toFloat(),
1405                     )
1406                     // Update freeform tasks
1407                     freeformTaskChanges.forEach {
1408                         val startX =
1409                             it.endAbsBounds.left +
1410                                 it.endAbsBounds.width() * (1 - freeformAnimScale) / 2
1411                         val startY =
1412                             it.endAbsBounds.top +
1413                                 it.endAbsBounds.height() * (1 - freeformAnimScale) / 2
1414                         setPosition(it.leash, startX, startY)
1415                         setScale(it.leash, freeformAnimScale, freeformAnimScale)
1416                         setAlpha(it.leash, freeformAnimFraction)
1417                     }
1418                 }
1419                 onTaskResizeAnimationListener.onBoundsChange(state.draggedTaskId, tx, animBounds)
1420             }
1421             .withEndActions({
1422                 onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
1423                 startTransitionFinishCb.onTransitionFinished(/* wct= */ null)
1424                 clearState()
1425                 interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
1426             })
1427             .start()
1428     }
1429 
1430     companion object {
1431         private const val TAG = "SpringDragToDesktopTransitionHandler"
1432 
1433         @VisibleForTesting
getAnimationFractionnull1434         fun getAnimationFraction(startBounds: Rect, endBounds: Rect, animBounds: Rect): Float {
1435             if (startBounds.width() != endBounds.width()) {
1436                 return (animBounds.width() - startBounds.width()).toFloat() /
1437                     (endBounds.width() - startBounds.width())
1438             }
1439             if (startBounds.height() != endBounds.height()) {
1440                 return (animBounds.height() - startBounds.height()).toFloat() /
1441                     (endBounds.height() - startBounds.height())
1442             }
1443             logW(
1444                 "same start and end sizes, returning 0: " +
1445                     "startBounds=$startBounds, endBounds=$endBounds, animBounds=$animBounds"
1446             )
1447             return 0f
1448         }
1449 
logVnull1450         private fun logV(msg: String, vararg arguments: Any?) {
1451             ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1452         }
1453 
logWnull1454         private fun logW(msg: String, vararg arguments: Any?) {
1455             ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1456         }
1457 
logEnull1458         private fun logE(msg: String, vararg arguments: Any?) {
1459             ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
1460         }
1461 
1462         /** The freeform tasks initial scale when committing the drag-to-desktop gesture. */
1463         private val FREEFORM_TASKS_INITIAL_SCALE =
1464             propertyValue("freeform_tasks_initial_scale", scale = 100f, default = 0.9f)
1465 
1466         /** The freeform tasks animation offset relative to the whole animation duration. */
1467         private val FREEFORM_TASKS_ANIM_OFFSET =
1468             propertyValue("freeform_tasks_anim_offset", scale = 100f, default = 0.5f)
1469 
1470         /** The spring force stiffness used to place the window into the final position. */
1471         private val POSITION_SPRING_STIFFNESS =
1472             propertyValue("position_stiffness", default = SpringForce.STIFFNESS_LOW)
1473 
1474         /** The spring force damping ratio used to place the window into the final position. */
1475         private val POSITION_SPRING_DAMPING_RATIO =
1476             propertyValue(
1477                 "position_damping_ratio",
1478                 scale = 100f,
1479                 default = SpringForce.DAMPING_RATIO_LOW_BOUNCY,
1480             )
1481 
1482         /** The spring force stiffness used to resize the window into the final bounds. */
1483         private val SIZE_SPRING_STIFFNESS =
1484             propertyValue("size_stiffness", default = SpringForce.STIFFNESS_LOW)
1485 
1486         /** The spring force damping ratio used to resize the window into the final bounds. */
1487         private val SIZE_SPRING_DAMPING_RATIO =
1488             propertyValue(
1489                 "size_damping_ratio",
1490                 scale = 100f,
1491                 default = SpringForce.DAMPING_RATIO_NO_BOUNCY,
1492             )
1493 
1494         /** Drag to desktop transition system properties group. */
1495         @VisibleForTesting
1496         const val SYSTEM_PROPERTIES_GROUP = "persist.wm.debug.desktop_transitions.drag_to_desktop"
1497 
1498         /**
1499          * Drag to desktop transition system property value with [name].
1500          *
1501          * @param scale an optional scale to apply to the value read from the system property.
1502          * @param default a default value to return if the system property isn't set.
1503          */
1504         @VisibleForTesting
propertyValuenull1505         fun propertyValue(name: String, scale: Float = 1f, default: Float = 0f): Float =
1506             SystemProperties.getInt(
1507                 /* key= */ "$SYSTEM_PROPERTIES_GROUP.$name",
1508                 /* def= */ (default * scale).toInt(),
1509             ) / scale
1510     }
1511 }
1512