<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