mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-28 15:56:49 +00:00
792 lines
37 KiB
Kotlin
792 lines
37 KiB
Kotlin
package com.android.wm.shell.desktopmode
|
|
|
|
import android.animation.Animator
|
|
import android.animation.AnimatorListenerAdapter
|
|
import android.animation.RectEvaluator
|
|
import android.animation.ValueAnimator
|
|
import android.app.ActivityManager.RunningTaskInfo
|
|
import android.app.ActivityOptions
|
|
import android.app.ActivityOptions.SourceInfo
|
|
import android.app.ActivityTaskManager.INVALID_TASK_ID
|
|
import android.app.PendingIntent
|
|
import android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
|
|
import android.app.PendingIntent.FLAG_MUTABLE
|
|
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
|
|
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
|
|
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.Intent.FILL_IN_COMPONENT
|
|
import android.graphics.PointF
|
|
import android.graphics.Rect
|
|
import android.os.Bundle
|
|
import android.os.IBinder
|
|
import android.os.SystemClock
|
|
import android.view.SurfaceControl
|
|
import android.view.WindowManager.TRANSIT_CLOSE
|
|
import android.window.TransitionInfo
|
|
import android.window.TransitionInfo.Change
|
|
import android.window.TransitionRequestInfo
|
|
import android.window.WindowContainerToken
|
|
import android.window.WindowContainerTransaction
|
|
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
|
|
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
|
|
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
|
|
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
|
|
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition
|
|
import com.android.wm.shell.protolog.ShellProtoLogGroup
|
|
import com.android.wm.shell.shared.TransitionUtil
|
|
import com.android.wm.shell.splitscreen.SplitScreenController
|
|
import com.android.wm.shell.transition.Transitions
|
|
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
|
|
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
|
|
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
|
|
import com.android.wm.shell.transition.Transitions.TransitionHandler
|
|
import com.android.wm.shell.util.KtProtoLog
|
|
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
|
|
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE
|
|
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
|
|
import java.util.function.Supplier
|
|
|
|
/**
|
|
* Handles the transition to enter desktop from fullscreen by dragging on the handle bar. It also
|
|
* handles the cancellation case where the task is dragged back to the status bar area in the same
|
|
* gesture.
|
|
*/
|
|
class DragToDesktopTransitionHandler(
|
|
private val context: Context,
|
|
private val transitions: Transitions,
|
|
private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
|
|
private val transactionSupplier: Supplier<SurfaceControl.Transaction>
|
|
) : TransitionHandler {
|
|
|
|
constructor(
|
|
context: Context,
|
|
transitions: Transitions,
|
|
rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
|
|
) : this(
|
|
context,
|
|
transitions,
|
|
rootTaskDisplayAreaOrganizer,
|
|
Supplier { SurfaceControl.Transaction() }
|
|
)
|
|
|
|
private val rectEvaluator = RectEvaluator(Rect())
|
|
private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
|
|
|
|
private var dragToDesktopStateListener: DragToDesktopStateListener? = null
|
|
private lateinit var splitScreenController: SplitScreenController
|
|
private var transitionState: TransitionState? = null
|
|
private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener
|
|
|
|
/** Whether a drag-to-desktop transition is in progress. */
|
|
val inProgress: Boolean
|
|
get() = transitionState != null
|
|
|
|
/** The task id of the task currently being dragged from fullscreen/split. */
|
|
val draggingTaskId: Int
|
|
get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID
|
|
/** Sets a listener to receive callback about events during the transition animation. */
|
|
fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) {
|
|
dragToDesktopStateListener = listener
|
|
}
|
|
|
|
/** Setter needed to avoid cyclic dependency. */
|
|
fun setSplitScreenController(controller: SplitScreenController) {
|
|
splitScreenController = controller
|
|
}
|
|
|
|
fun setOnTaskResizeAnimatorListener(listener: OnTaskResizeAnimationListener) {
|
|
onTaskResizeAnimationListener = listener
|
|
}
|
|
|
|
/**
|
|
* Starts a transition that performs a transient launch of Home so that Home is brought to the
|
|
* front while still keeping the currently focused task that is being dragged resumed. This
|
|
* allows the animation handler to reorder the task to the front and to scale it with the
|
|
* gesture into the desktop area with the Home and wallpaper behind it.
|
|
*
|
|
* Note that the transition handler for this transition doesn't call the finish callback until
|
|
* after one of the "end" or "cancel" transitions is merged into this transition.
|
|
*/
|
|
fun startDragToDesktopTransition(
|
|
taskId: Int,
|
|
dragToDesktopAnimator: MoveToDesktopAnimator,
|
|
) {
|
|
if (inProgress) {
|
|
KtProtoLog.v(
|
|
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
|
|
"DragToDesktop: Drag to desktop transition already in progress."
|
|
)
|
|
return
|
|
}
|
|
|
|
val options =
|
|
ActivityOptions.makeBasic().apply {
|
|
setTransientLaunch()
|
|
setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
|
|
pendingIntentCreatorBackgroundActivityStartMode =
|
|
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
|
|
}
|
|
val pendingIntent =
|
|
PendingIntent.getActivity(
|
|
context,
|
|
0 /* requestCode */,
|
|
launchHomeIntent,
|
|
FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
|
|
options.toBundle()
|
|
)
|
|
val wct = WindowContainerTransaction()
|
|
wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle())
|
|
val startTransitionToken =
|
|
transitions.startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
|
|
|
|
transitionState =
|
|
if (isSplitTask(taskId)) {
|
|
val otherTask =
|
|
getOtherSplitTask(taskId)
|
|
?: throw IllegalStateException("Expected split task to have a counterpart.")
|
|
TransitionState.FromSplit(
|
|
draggedTaskId = taskId,
|
|
dragAnimator = dragToDesktopAnimator,
|
|
startTransitionToken = startTransitionToken,
|
|
otherSplitTask = otherTask
|
|
)
|
|
} else {
|
|
TransitionState.FromFullscreen(
|
|
draggedTaskId = taskId,
|
|
dragAnimator = dragToDesktopAnimator,
|
|
startTransitionToken = startTransitionToken
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts a transition that "finishes" the drag to desktop gesture. This transition is intended
|
|
* to merge into the "start" transition and is the one that actually applies the bounds and
|
|
* windowing mode changes to the dragged task. This is called when the dragged task is released
|
|
* inside the desktop drop zone.
|
|
*/
|
|
fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? {
|
|
if (!inProgress) {
|
|
// Don't attempt to finish a drag to desktop transition since there is no transition in
|
|
// progress which means that the drag to desktop transition was never successfully
|
|
// started.
|
|
return null
|
|
}
|
|
if (requireTransitionState().startAborted) {
|
|
// Don't attempt to complete the drag-to-desktop since the start transition didn't
|
|
// succeed as expected. Just reset the state as if nothing happened.
|
|
clearState()
|
|
return null
|
|
}
|
|
return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
|
|
}
|
|
|
|
/**
|
|
* Starts a transition that "cancels" the drag to desktop gesture. This transition is intended
|
|
* to merge into the "start" transition and it restores the transient state that was used to
|
|
* launch the Home task over the dragged task. This is called when the dragged task is released
|
|
* outside the desktop drop zone and is instead dropped back into the status bar region that
|
|
* means the user wants to remain in their current windowing mode.
|
|
*/
|
|
fun cancelDragToDesktopTransition(cancelState: CancelState) {
|
|
if (!inProgress) {
|
|
// Don't attempt to cancel a drag to desktop transition since there is no transition in
|
|
// progress which means that the drag to desktop transition was never successfully
|
|
// started.
|
|
return
|
|
}
|
|
val state = requireTransitionState()
|
|
if (state.startAborted) {
|
|
// Don't attempt to cancel the drag-to-desktop since the start transition didn't
|
|
// succeed as expected. Just reset the state as if nothing happened.
|
|
clearState()
|
|
return
|
|
}
|
|
state.cancelState = cancelState
|
|
|
|
if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
|
|
// Regular case, transient launch of Home happened as is waiting for the cancel
|
|
// transient to start and merge. Animate the cancellation (scale back to original
|
|
// bounds) first before actually starting the cancel transition so that the wallpaper
|
|
// is visible behind the animating task.
|
|
startCancelAnimation()
|
|
} else if (
|
|
state.draggedTaskChange != null &&
|
|
(cancelState == CancelState.CANCEL_SPLIT_LEFT ||
|
|
cancelState == CancelState.CANCEL_SPLIT_RIGHT)
|
|
) {
|
|
// We have a valid dragged task, but the animation will be handled by
|
|
// SplitScreenController; request the transition here.
|
|
@SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) {
|
|
SPLIT_POSITION_TOP_OR_LEFT
|
|
} else {
|
|
SPLIT_POSITION_BOTTOM_OR_RIGHT
|
|
}
|
|
val wct = WindowContainerTransaction()
|
|
restoreWindowOrder(wct, state)
|
|
state.startTransitionFinishTransaction?.apply()
|
|
state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
|
|
requestSplitFromScaledTask(splitPosition, wct)
|
|
clearState()
|
|
} else {
|
|
// There's no dragged task, this can happen when the "cancel" happened too quickly
|
|
// before the "start" transition is even ready (like on a fling gesture). The
|
|
// "shrink" animation didn't even start, so there's no need to animate the "cancel".
|
|
// We also don't want to start the cancel transition yet since we don't have
|
|
// enough info to restore the order. We'll check for the cancelled state flag when
|
|
// the "start" animation is ready and cancel from #startAnimation instead.
|
|
}
|
|
}
|
|
|
|
/** Calculate the bounds of a scaled task, then use those bounds to request split select. */
|
|
private fun requestSplitFromScaledTask(
|
|
@SplitPosition splitPosition: Int,
|
|
wct: WindowContainerTransaction
|
|
) {
|
|
val state = requireTransitionState()
|
|
val taskInfo = state.draggedTaskChange?.taskInfo
|
|
?: error("Expected non-null taskInfo")
|
|
val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds)
|
|
val taskScale = state.dragAnimator.scale
|
|
val scaledWidth = taskBounds.width() * taskScale
|
|
val scaledHeight = taskBounds.height() * taskScale
|
|
val dragPosition = PointF(state.dragAnimator.position)
|
|
state.dragAnimator.cancelAnimator()
|
|
val animatedTaskBounds = Rect(
|
|
dragPosition.x.toInt(),
|
|
dragPosition.y.toInt(),
|
|
(dragPosition.x + scaledWidth).toInt(),
|
|
(dragPosition.y + scaledHeight).toInt()
|
|
)
|
|
requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds)
|
|
}
|
|
|
|
private fun requestSplitSelect(
|
|
wct: WindowContainerTransaction,
|
|
taskInfo: RunningTaskInfo,
|
|
@SplitPosition splitPosition: Int,
|
|
taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds)
|
|
) {
|
|
// Prepare to exit split in order to enter split select.
|
|
if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
|
|
splitScreenController.prepareExitSplitScreen(
|
|
wct,
|
|
splitScreenController.getStageOfTask(taskInfo.taskId),
|
|
SplitScreenController.EXIT_REASON_DESKTOP_MODE
|
|
)
|
|
splitScreenController.transitionHandler.onSplitToDesktop()
|
|
}
|
|
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
|
|
wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi)
|
|
splitScreenController.requestEnterSplitSelect(
|
|
taskInfo,
|
|
wct,
|
|
splitPosition,
|
|
taskBounds
|
|
)
|
|
}
|
|
|
|
override fun startAnimation(
|
|
transition: IBinder,
|
|
info: TransitionInfo,
|
|
startTransaction: SurfaceControl.Transaction,
|
|
finishTransaction: SurfaceControl.Transaction,
|
|
finishCallback: Transitions.TransitionFinishCallback
|
|
): Boolean {
|
|
val state = requireTransitionState()
|
|
|
|
val isStartDragToDesktop =
|
|
info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
|
|
transition == state.startTransitionToken
|
|
if (!isStartDragToDesktop) {
|
|
return false
|
|
}
|
|
|
|
// Layering: non-wallpaper, non-home tasks excluding the dragged task go at the bottom,
|
|
// then Home on top of that, wallpaper on top of that and finally the dragged task on top
|
|
// of everything.
|
|
val appLayers = info.changes.size
|
|
val homeLayers = info.changes.size * 2
|
|
val wallpaperLayers = info.changes.size * 3
|
|
val dragLayer = wallpaperLayers
|
|
val leafTaskFilter = TransitionUtil.LeafTaskFilter()
|
|
info.changes.withIndex().forEach { (i, change) ->
|
|
if (TransitionUtil.isWallpaper(change)) {
|
|
val layer = wallpaperLayers - i
|
|
startTransaction.apply {
|
|
setLayer(change.leash, layer)
|
|
show(change.leash)
|
|
}
|
|
} else if (isHomeChange(change)) {
|
|
state.homeToken = change.container
|
|
val layer = homeLayers - i
|
|
startTransaction.apply {
|
|
setLayer(change.leash, layer)
|
|
show(change.leash)
|
|
}
|
|
} else if (TransitionInfo.isIndependent(change, info)) {
|
|
// Root(s).
|
|
when (state) {
|
|
is TransitionState.FromSplit -> {
|
|
state.splitRootChange = change
|
|
val layer =
|
|
if (state.cancelState == CancelState.NO_CANCEL) {
|
|
// Normal case, split root goes to the bottom behind everything
|
|
// else.
|
|
appLayers - i
|
|
} else {
|
|
// Cancel-early case, pretend nothing happened so split root stays
|
|
// top.
|
|
dragLayer
|
|
}
|
|
startTransaction.apply {
|
|
setLayer(change.leash, layer)
|
|
show(change.leash)
|
|
}
|
|
}
|
|
is TransitionState.FromFullscreen -> {
|
|
// Most of the time we expect one change/task here, which should be the
|
|
// same that initiated the drag and that should be layered on top of
|
|
// everything.
|
|
if (change.taskInfo?.taskId == state.draggedTaskId) {
|
|
state.draggedTaskChange = change
|
|
val bounds = change.endAbsBounds
|
|
startTransaction.apply {
|
|
setLayer(change.leash, dragLayer)
|
|
setWindowCrop(change.leash, bounds.width(), bounds.height())
|
|
show(change.leash)
|
|
}
|
|
} else {
|
|
// It's possible to see an additional change that isn't the dragged
|
|
// task when the dragged task is translucent and so the task behind it
|
|
// is included in the transition since it was visible and is now being
|
|
// occluded by the Home task. Just layer it at the bottom and save it
|
|
// in case we need to restore order if the drag is cancelled.
|
|
state.otherRootChanges.add(change)
|
|
val bounds = change.endAbsBounds
|
|
startTransaction.apply {
|
|
setLayer(change.leash, appLayers - i)
|
|
setWindowCrop(change.leash, bounds.width(), bounds.height())
|
|
show(change.leash)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (leafTaskFilter.test(change)) {
|
|
// When dragging one of the split tasks, the dragged leaf needs to be re-parented
|
|
// so that it can be layered separately from the rest of the split root/stages.
|
|
// The split root including the other split side was layered behind the wallpaper
|
|
// and home while the dragged split needs to be layered in front of them.
|
|
// Do not do this in the cancel-early case though, since in that case nothing should
|
|
// happen on screen so the layering will remain the same as if no transition
|
|
// occurred.
|
|
if (
|
|
change.taskInfo?.taskId == state.draggedTaskId &&
|
|
state.cancelState != CancelState.STANDARD_CANCEL
|
|
) {
|
|
// We need access to the dragged task's change in both non-cancel and split
|
|
// cancel cases.
|
|
state.draggedTaskChange = change
|
|
}
|
|
if (
|
|
change.taskInfo?.taskId == state.draggedTaskId &&
|
|
state.cancelState == CancelState.NO_CANCEL
|
|
) {
|
|
taskDisplayAreaOrganizer.reparentToDisplayArea(
|
|
change.endDisplayId,
|
|
change.leash,
|
|
startTransaction
|
|
)
|
|
val bounds = change.endAbsBounds
|
|
startTransaction.apply {
|
|
setLayer(change.leash, dragLayer)
|
|
setWindowCrop(change.leash, bounds.width(), bounds.height())
|
|
show(change.leash)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
state.startTransitionFinishCb = finishCallback
|
|
state.startTransitionFinishTransaction = finishTransaction
|
|
startTransaction.apply()
|
|
|
|
if (state.cancelState == CancelState.NO_CANCEL) {
|
|
// Normal case, start animation to scale down the dragged task. It'll also be moved to
|
|
// follow the finger and when released we'll start the next phase/transition.
|
|
state.dragAnimator.startAnimation()
|
|
} else if (state.cancelState == CancelState.STANDARD_CANCEL) {
|
|
// Cancel-early case, the state was flagged was cancelled already, which means the
|
|
// gesture ended in the cancel region. This can happen even before the start transition
|
|
// is ready/animate here when cancelling quickly like with a fling. There's no point
|
|
// in starting the scale down animation that we would scale up anyway, so just jump
|
|
// directly into starting the cancel transition to restore WM order. Surfaces should
|
|
// not move as if no transition happened.
|
|
startCancelDragToDesktopTransition()
|
|
} else if (
|
|
state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
|
|
state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
|
|
){
|
|
// Cancel-early case for split-cancel. The state was flagged already as a cancel for
|
|
// requesting split select. Similar to the above, this can happen due to quick fling
|
|
// gestures. We can simply request split here without needing to calculate animated
|
|
// task bounds as the task has not shrunk at all.
|
|
val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) {
|
|
SPLIT_POSITION_TOP_OR_LEFT
|
|
} else {
|
|
SPLIT_POSITION_BOTTOM_OR_RIGHT
|
|
}
|
|
val taskInfo = state.draggedTaskChange?.taskInfo
|
|
?: error("Expected non-null task info.")
|
|
val wct = WindowContainerTransaction()
|
|
restoreWindowOrder(wct)
|
|
state.startTransitionFinishTransaction?.apply()
|
|
state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
|
|
requestSplitSelect(wct, taskInfo, splitPosition)
|
|
}
|
|
return true
|
|
}
|
|
|
|
override fun mergeAnimation(
|
|
transition: IBinder,
|
|
info: TransitionInfo,
|
|
t: SurfaceControl.Transaction,
|
|
mergeTarget: IBinder,
|
|
finishCallback: Transitions.TransitionFinishCallback
|
|
) {
|
|
val state = requireTransitionState()
|
|
// We don't want to merge the split select animation if that's what we requested.
|
|
if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
|
|
state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) {
|
|
clearState()
|
|
return
|
|
}
|
|
val isCancelTransition =
|
|
info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
|
|
transition == state.cancelTransitionToken &&
|
|
mergeTarget == state.startTransitionToken
|
|
val isEndTransition =
|
|
info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
|
|
mergeTarget == state.startTransitionToken
|
|
|
|
val startTransactionFinishT =
|
|
state.startTransitionFinishTransaction
|
|
?: error("Start transition expected to be waiting for merge but wasn't")
|
|
val startTransitionFinishCb =
|
|
state.startTransitionFinishCb
|
|
?: error("Start transition expected to be waiting for merge but wasn't")
|
|
if (isEndTransition) {
|
|
info.changes.withIndex().forEach { (i, change) ->
|
|
// If we're exiting split, hide the remaining split task.
|
|
if (
|
|
state is TransitionState.FromSplit &&
|
|
change.taskInfo?.taskId == state.otherSplitTask
|
|
) {
|
|
t.hide(change.leash)
|
|
startTransactionFinishT.hide(change.leash)
|
|
}
|
|
if (change.mode == TRANSIT_CLOSE) {
|
|
t.hide(change.leash)
|
|
startTransactionFinishT.hide(change.leash)
|
|
} else if (change.taskInfo?.taskId == state.draggedTaskId) {
|
|
t.show(change.leash)
|
|
startTransactionFinishT.show(change.leash)
|
|
state.draggedTaskChange = change
|
|
} else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) {
|
|
// Other freeform tasks that are being restored go behind the dragged task.
|
|
val draggedTaskLeash =
|
|
state.draggedTaskChange?.leash
|
|
?: error("Expected dragged leash to be non-null")
|
|
t.setRelativeLayer(change.leash, draggedTaskLeash, -i)
|
|
startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i)
|
|
}
|
|
}
|
|
|
|
val draggedTaskChange =
|
|
state.draggedTaskChange
|
|
?: throw IllegalStateException("Expected non-null change of dragged task")
|
|
val draggedTaskLeash = draggedTaskChange.leash
|
|
val startBounds = draggedTaskChange.startAbsBounds
|
|
val endBounds = draggedTaskChange.endAbsBounds
|
|
|
|
// Pause any animation that may be currently playing; we will use the relevant
|
|
// details of that animation here.
|
|
state.dragAnimator.cancelAnimator()
|
|
// We still apply scale to task bounds; as we animate the bounds to their
|
|
// end value, animate scale to 1.
|
|
val startScale = state.dragAnimator.scale
|
|
val startPosition = state.dragAnimator.position
|
|
val unscaledStartWidth = startBounds.width()
|
|
val unscaledStartHeight = startBounds.height()
|
|
val unscaledStartBounds =
|
|
Rect(
|
|
startPosition.x.toInt(),
|
|
startPosition.y.toInt(),
|
|
startPosition.x.toInt() + unscaledStartWidth,
|
|
startPosition.y.toInt() + unscaledStartHeight
|
|
)
|
|
|
|
dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t)
|
|
// Accept the merge by applying the merging transaction (applied by #showResizeVeil)
|
|
// and finish callback. Show the veil and position the task at the first frame before
|
|
// starting the final animation.
|
|
onTaskResizeAnimationListener.onAnimationStart(
|
|
state.draggedTaskId,
|
|
t,
|
|
unscaledStartBounds
|
|
)
|
|
finishCallback.onTransitionFinished(null /* wct */)
|
|
val tx: SurfaceControl.Transaction = transactionSupplier.get()
|
|
ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds)
|
|
.setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
|
|
.apply {
|
|
addUpdateListener { animator ->
|
|
val animBounds = animator.animatedValue as Rect
|
|
val animFraction = animator.animatedFraction
|
|
// Progress scale from starting value to 1 as animation plays.
|
|
val animScale = startScale + animFraction * (1 - startScale)
|
|
tx.apply {
|
|
setScale(draggedTaskLeash, animScale, animScale)
|
|
setPosition(
|
|
draggedTaskLeash,
|
|
animBounds.left.toFloat(),
|
|
animBounds.top.toFloat()
|
|
)
|
|
setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height())
|
|
}
|
|
onTaskResizeAnimationListener.onBoundsChange(
|
|
state.draggedTaskId,
|
|
tx,
|
|
animBounds
|
|
)
|
|
}
|
|
addListener(
|
|
object : AnimatorListenerAdapter() {
|
|
override fun onAnimationEnd(animation: Animator) {
|
|
onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
|
|
startTransitionFinishCb.onTransitionFinished(null /* null */)
|
|
clearState()
|
|
}
|
|
}
|
|
)
|
|
start()
|
|
}
|
|
} else if (isCancelTransition) {
|
|
info.changes.forEach { change ->
|
|
t.show(change.leash)
|
|
startTransactionFinishT.show(change.leash)
|
|
}
|
|
t.apply()
|
|
finishCallback.onTransitionFinished(null /* wct */)
|
|
startTransitionFinishCb.onTransitionFinished(null /* wct */)
|
|
clearState()
|
|
}
|
|
}
|
|
|
|
override fun handleRequest(
|
|
transition: IBinder,
|
|
request: TransitionRequestInfo
|
|
): WindowContainerTransaction? {
|
|
// Only handle transitions started from shell.
|
|
return null
|
|
}
|
|
|
|
override fun onTransitionConsumed(
|
|
transition: IBinder,
|
|
aborted: Boolean,
|
|
finishTransaction: SurfaceControl.Transaction?
|
|
) {
|
|
val state = transitionState ?: return
|
|
if (aborted && state.startTransitionToken == transition) {
|
|
KtProtoLog.v(
|
|
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
|
|
"DragToDesktop: onTransitionConsumed() start transition aborted"
|
|
)
|
|
state.startAborted = true
|
|
}
|
|
}
|
|
|
|
private fun isHomeChange(change: Change): Boolean {
|
|
return change.taskInfo?.activityType == ACTIVITY_TYPE_HOME
|
|
}
|
|
|
|
private fun startCancelAnimation() {
|
|
val state = requireTransitionState()
|
|
val dragToDesktopAnimator = state.dragAnimator
|
|
|
|
val draggedTaskChange =
|
|
state.draggedTaskChange ?: throw IllegalStateException("Expected non-null task change")
|
|
val sc = draggedTaskChange.leash
|
|
// Pause the animation that shrinks the window when task is first dragged from fullscreen
|
|
dragToDesktopAnimator.cancelAnimator()
|
|
// Then animate the scaled window back to its original bounds.
|
|
val x: Float = dragToDesktopAnimator.position.x
|
|
val y: Float = dragToDesktopAnimator.position.y
|
|
val targetX = draggedTaskChange.endAbsBounds.left
|
|
val targetY = draggedTaskChange.endAbsBounds.top
|
|
val dx = targetX - x
|
|
val dy = targetY - y
|
|
val tx: SurfaceControl.Transaction = transactionSupplier.get()
|
|
ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
|
|
.setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
|
|
.apply {
|
|
addUpdateListener { animator ->
|
|
val scale = animator.animatedValue as Float
|
|
val fraction = animator.animatedFraction
|
|
val animX = x + (dx * fraction)
|
|
val animY = y + (dy * fraction)
|
|
tx.apply {
|
|
setPosition(sc, animX, animY)
|
|
setScale(sc, scale, scale)
|
|
show(sc)
|
|
apply()
|
|
}
|
|
}
|
|
addListener(
|
|
object : AnimatorListenerAdapter() {
|
|
override fun onAnimationEnd(animation: Animator) {
|
|
dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx)
|
|
// Start the cancel transition to restore order.
|
|
startCancelDragToDesktopTransition()
|
|
}
|
|
}
|
|
)
|
|
start()
|
|
}
|
|
}
|
|
|
|
private fun startCancelDragToDesktopTransition() {
|
|
val state = requireTransitionState()
|
|
val wct = WindowContainerTransaction()
|
|
restoreWindowOrder(wct, state)
|
|
state.cancelTransitionToken =
|
|
transitions.startTransition(
|
|
TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this
|
|
)
|
|
}
|
|
|
|
private fun restoreWindowOrder(
|
|
wct: WindowContainerTransaction,
|
|
state: TransitionState = requireTransitionState()
|
|
) {
|
|
when (state) {
|
|
is TransitionState.FromFullscreen -> {
|
|
// There may have been tasks sent behind home that are not the dragged task (like
|
|
// when the dragged task is translucent and that makes the task behind it visible).
|
|
// Restore the order of those first.
|
|
state.otherRootChanges
|
|
.mapNotNull { it.container }
|
|
.forEach { wc ->
|
|
// TODO(b/322852244): investigate why even though these "other" tasks are
|
|
// reordered in front of home and behind the translucent dragged task, its
|
|
// surface is not visible on screen.
|
|
wct.reorder(wc, true /* toTop */)
|
|
}
|
|
val wc =
|
|
state.draggedTaskChange?.container
|
|
?: error("Dragged task should be non-null before cancelling")
|
|
// Then the dragged task a the very top.
|
|
wct.reorder(wc, true /* toTop */)
|
|
}
|
|
is TransitionState.FromSplit -> {
|
|
val wc =
|
|
state.splitRootChange?.container
|
|
?: error("Split root should be non-null before cancelling")
|
|
wct.reorder(wc, true /* toTop */)
|
|
}
|
|
}
|
|
val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling")
|
|
wct.restoreTransientOrder(homeWc)
|
|
}
|
|
|
|
private fun clearState() {
|
|
transitionState = null
|
|
}
|
|
|
|
private fun isSplitTask(taskId: Int): Boolean {
|
|
return splitScreenController.isTaskInSplitScreen(taskId)
|
|
}
|
|
|
|
private fun getOtherSplitTask(taskId: Int): Int? {
|
|
val splitPos = splitScreenController.getSplitPosition(taskId)
|
|
if (splitPos == SPLIT_POSITION_UNDEFINED) return null
|
|
val otherTaskPos =
|
|
if (splitPos == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
|
|
SPLIT_POSITION_TOP_OR_LEFT
|
|
} else {
|
|
SPLIT_POSITION_BOTTOM_OR_RIGHT
|
|
}
|
|
return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
|
|
}
|
|
|
|
private fun requireTransitionState(): TransitionState {
|
|
return transitionState ?: error("Expected non-null transition state")
|
|
}
|
|
|
|
interface DragToDesktopStateListener {
|
|
fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction)
|
|
|
|
fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction)
|
|
}
|
|
|
|
sealed class TransitionState {
|
|
abstract val draggedTaskId: Int
|
|
abstract val dragAnimator: MoveToDesktopAnimator
|
|
abstract val startTransitionToken: IBinder
|
|
abstract var startTransitionFinishCb: Transitions.TransitionFinishCallback?
|
|
abstract var startTransitionFinishTransaction: SurfaceControl.Transaction?
|
|
abstract var cancelTransitionToken: IBinder?
|
|
abstract var homeToken: WindowContainerToken?
|
|
abstract var draggedTaskChange: Change?
|
|
abstract var cancelState: CancelState
|
|
abstract var startAborted: Boolean
|
|
|
|
data class FromFullscreen(
|
|
override val draggedTaskId: Int,
|
|
override val dragAnimator: MoveToDesktopAnimator,
|
|
override val startTransitionToken: IBinder,
|
|
override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
|
|
override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
|
|
override var cancelTransitionToken: IBinder? = null,
|
|
override var homeToken: WindowContainerToken? = null,
|
|
override var draggedTaskChange: Change? = null,
|
|
override var cancelState: CancelState = CancelState.NO_CANCEL,
|
|
override var startAborted: Boolean = false,
|
|
var otherRootChanges: MutableList<Change> = mutableListOf()
|
|
) : TransitionState()
|
|
|
|
data class FromSplit(
|
|
override val draggedTaskId: Int,
|
|
override val dragAnimator: MoveToDesktopAnimator,
|
|
override val startTransitionToken: IBinder,
|
|
override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
|
|
override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
|
|
override var cancelTransitionToken: IBinder? = null,
|
|
override var homeToken: WindowContainerToken? = null,
|
|
override var draggedTaskChange: Change? = null,
|
|
override var cancelState: CancelState = CancelState.NO_CANCEL,
|
|
override var startAborted: Boolean = false,
|
|
var splitRootChange: Change? = null,
|
|
var otherSplitTask: Int
|
|
) : TransitionState()
|
|
}
|
|
|
|
/** Enum to provide context on cancelling a drag to desktop event. */
|
|
enum class CancelState {
|
|
/** No cancel case; this drag is not flagged for a cancel event. */
|
|
NO_CANCEL,
|
|
/** A standard cancel event; should restore task to previous windowing mode. */
|
|
STANDARD_CANCEL,
|
|
/** A cancel event where the task will request to enter split on the left side. */
|
|
CANCEL_SPLIT_LEFT,
|
|
/** A cancel event where the task will request to enter split on the right side. */
|
|
CANCEL_SPLIT_RIGHT
|
|
}
|
|
|
|
companion object {
|
|
/** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
|
|
private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
|
|
}
|
|
}
|