<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