Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +4 −0 Original line number Diff line number Diff line Loading @@ -238,6 +238,10 @@ class DesktopTasksController( removeVisualIndicator() } override fun onTransitionInterrupted() { removeVisualIndicator() } private fun removeVisualIndicator() { visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +144 −6 Original line number Diff line number Diff line Loading @@ -24,8 +24,10 @@ import android.os.SystemClock import android.os.SystemProperties import android.os.UserHandle import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CLOSE import android.window.DesktopModeFlags import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo Loading Loading @@ -185,18 +187,29 @@ sealed class DragToDesktopTransitionHandler( */ fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? { if (!inProgress) { logV("finishDragToDesktop: not in progress, returning") // 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) { val state = requireTransitionState() if (state.startAborted) { logV("finishDragToDesktop: start was aborted, clearing state") // 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) if (state.startInterrupted) { logV("finishDragToDesktop: start was interrupted, returning") // We should only have interrupted the start transition after receiving a cancel/end // request, let that existing request play out and just return here. return null } state.endTransitionToken = transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) return state.endTransitionToken } /** Loading @@ -220,6 +233,11 @@ sealed class DragToDesktopTransitionHandler( clearState() return } if (state.startInterrupted) { // We should only have interrupted the start transition after receiving a cancel/end // request, let that existing request play out and just return here. return } state.cancelState = cancelState if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) { Loading @@ -227,7 +245,7 @@ sealed class DragToDesktopTransitionHandler( // 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() state.activeCancelAnimation = startCancelAnimation() } else if ( state.draggedTaskChange != null && (cancelState == CancelState.CANCEL_SPLIT_LEFT || Loading Loading @@ -255,7 +273,7 @@ sealed class DragToDesktopTransitionHandler( ) { if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { // TODO(b/388853233): add support for dragging split task to bubble startCancelAnimation() state.activeCancelAnimation = startCancelAnimation() } else { // Animation is handled by BubbleController val wct = WindowContainerTransaction() Loading Loading @@ -357,6 +375,19 @@ sealed class DragToDesktopTransitionHandler( ): Boolean { val state = requireTransitionState() if ( handleCancelOrExitAfterInterrupt( transition, info, startTransaction, finishTransaction, finishCallback, state, ) ) { return true } val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP && transition == state.startTransitionToken Loading Loading @@ -539,6 +570,58 @@ sealed class DragToDesktopTransitionHandler( } } private fun handleCancelOrExitAfterInterrupt( transition: IBinder, info: TransitionInfo, startTransaction: Transaction, finishTransaction: Transaction, finishCallback: Transitions.TransitionFinishCallback, state: TransitionState, ): Boolean { if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { return false } val isCancelDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && transition == state.cancelTransitionToken val isEndDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP && transition == state.endTransitionToken // We should only receive cancel or end transitions through startAnimation() if the // start transition was interrupted while a cancel- or end-transition had already // been requested. Finish the cancel/end transition to avoid having to deal with more // incoming transitions, and clear the state for the next start-drag transition. if (!isCancelDragToDesktop && !isEndDragToDesktop) { return false } if (!state.startInterrupted) { logW( "Not interrupted, but received startAnimation for cancel/end drag." + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" ) return false } logV( "startAnimation: interrupted -> " + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" ) if (isEndDragToDesktop) { setupEndDragToDesktop(info, startTransaction, finishTransaction) animateEndDragToDesktop(startTransaction = startTransaction, finishCallback) } else { // isCancelDragToDesktop // Similar to when we merge the cancel transition: ensure all tasks involved in the // cancel transition are shown, and finish the transition immediately. info.changes.forEach { change -> startTransaction.show(change.leash) finishTransaction.show(change.leash) } } startTransaction.apply() finishCallback.onTransitionFinished(/* wct= */ null) clearState() return true } /** * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is Loading Loading @@ -590,6 +673,7 @@ sealed class DragToDesktopTransitionHandler( ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { logV("mergeAnimation: end-transition, target=$mergeTarget") state.mergedEndTransition = true setupEndDragToDesktop( info, startTransaction = startT, Loading Loading @@ -617,6 +701,41 @@ sealed class DragToDesktopTransitionHandler( return } logW("unhandled merge transition: transitionInfo=$info") // Handle unknown incoming transitions by finishing the start transition. For now, only do // this if we've already requested a cancel- or end transition. If we've already merged the // end-transition, or if the end-transition is running on its own, then just wait until that // finishes instead. If we've merged the cancel-transition we've finished the // start-transition and won't reach this code. if ( mergeTarget == state.startTransitionToken && isCancelOrEndTransitionRequested(state) && !state.mergedEndTransition ) { interruptStartTransition(state) } } private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean = state.cancelTransitionToken != null || state.endTransitionToken != null private fun interruptStartTransition(state: TransitionState) { if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { return } logV("interruptStartTransition") state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) state.dragAnimator.cancelAnimator() state.activeCancelAnimation?.removeAllListeners() state.activeCancelAnimation?.cancel() state.activeCancelAnimation = null // Keep the transition state so we can deal with Cancel/End properly in #startAnimation. state.startInterrupted = true dragToDesktopStateListener?.onTransitionInterrupted() // Cancel CUJs here as they won't be accurate now that an incoming transition is playing. interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) LatencyTracker.getInstance(context) .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) } protected open fun setupEndDragToDesktop( Loading Loading @@ -783,7 +902,7 @@ sealed class DragToDesktopTransitionHandler( } ?: false } private fun startCancelAnimation() { private fun startCancelAnimation(): Animator { val state = requireTransitionState() val dragToDesktopAnimator = state.dragAnimator Loading @@ -800,7 +919,7 @@ sealed class DragToDesktopTransitionHandler( val dx = targetX - x val dy = targetY - y val tx: SurfaceControl.Transaction = transactionSupplier.get() ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) .apply { addUpdateListener { animator -> Loading @@ -818,6 +937,7 @@ sealed class DragToDesktopTransitionHandler( addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { state.activeCancelAnimation = null dragToDesktopStateListener?.onCancelToDesktopAnimationEnd() // Start the cancel transition to restore order. startCancelDragToDesktopTransition() Loading Loading @@ -910,10 +1030,16 @@ sealed class DragToDesktopTransitionHandler( val dragLayer: Int, ) /** Listener for various events happening during the DragToDesktop transition. */ interface DragToDesktopStateListener { /** Indicates that the animation into Desktop has started. */ fun onCommitToDesktopAnimationStart() /** Called when the animation to cancel the desktop-drag has finished. */ fun onCancelToDesktopAnimationEnd() /** Indicates that the drag-to-desktop transition has been interrupted. */ fun onTransitionInterrupted() } sealed class TransitionState { Loading @@ -930,6 +1056,10 @@ sealed class DragToDesktopTransitionHandler( abstract var cancelState: CancelState abstract var startAborted: Boolean abstract val visualIndicator: DesktopModeVisualIndicator? abstract var startInterrupted: Boolean abstract var endTransitionToken: IBinder? abstract var mergedEndTransition: Boolean abstract var activeCancelAnimation: Animator? data class FromFullscreen( override val draggedTaskId: Int, Loading @@ -945,6 +1075,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, override var startInterrupted: Boolean = false, override var endTransitionToken: IBinder? = null, override var mergedEndTransition: Boolean = false, override var activeCancelAnimation: Animator? = null, var otherRootChanges: MutableList<Change> = mutableListOf(), ) : TransitionState() Loading @@ -962,6 +1096,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, override var startInterrupted: Boolean = false, override var endTransitionToken: IBinder? = null, override var mergedEndTransition: Boolean = false, override var activeCancelAnimation: Animator? = null, var splitRootChange: Change? = null, var otherSplitTask: Int, ) : TransitionState() Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +133 −10 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder Loading @@ -34,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.CancelState import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT Loading @@ -56,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.MockitoSession import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.never Loading Loading @@ -118,6 +121,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .mockStatic(SystemProperties::class.java) .startMocking() whenever( transitions.startTransition( eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), /* wct= */ any(), eq(defaultHandler), ) ) .thenReturn(mock<IBinder>()) } @After Loading Loading @@ -679,17 +690,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val startTransition = startDrag(defaultHandler, task) val endTransition = mock<IBinder>() defaultHandler.onTaskResizeAnimationListener = mock() defaultHandler.mergeAnimation( mergeAnimation( transition = endTransition, info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), startT = mock<SurfaceControl.Transaction>(), finishT = mock<SurfaceControl.Transaction>(), task = task, mergeTarget = startTransition, finishCallback = mock<Transitions.TransitionFinishCallback>(), ) defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock()) Loading @@ -700,6 +705,123 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)) } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() { val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() defaultHandler.onTaskResizeAnimationListener = mock() val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback) mergeInterruptingTransition(mergeTarget = startTransition) verify(finishCallback, never()).onTransitionFinished(anyOrNull()) verify(dragAnimator, never()).cancelAnimator() } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() { val startDragFinishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() val startTransition = startDrag(defaultHandler, task, finishCallback = startDragFinishCallback) defaultHandler.onTaskResizeAnimationListener = mock() mergeAnimation( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, task = task, mergeTarget = startTransition, ) mergeInterruptingTransition(mergeTarget = startTransition) verify(startDragFinishCallback, never()).onTransitionFinished(anyOrNull()) } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun startEndAnimation_otherTransitionInterruptedStartAfterEndRequest_finishImmediately() { val task1 = createTask() val startTransition = startDrag(defaultHandler, task1) val endTransition = defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) val startTransaction = mock<SurfaceControl.Transaction>() val endDragFinishCallback = mock<Transitions.TransitionFinishCallback>() defaultHandler.onTaskResizeAnimationListener = mock() mergeInterruptingTransition(mergeTarget = startTransition) val didAnimate = defaultHandler.startAnimation( transition = requireNotNull(endTransition), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task1, ), startTransaction = startTransaction, finishTransaction = mock(), finishCallback = endDragFinishCallback, ) assertThat(didAnimate).isTrue() verify(startTransaction).apply() verify(endDragFinishCallback).onTransitionFinished(anyOrNull()) } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun startDrag_otherTransitionInterruptedStartAfterEndRequested_animatesDragWhenReady() { val task1 = createTask() val startTransition = startDrag(defaultHandler, task1) verify(dragAnimator).startAnimation() val endTransition = defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) defaultHandler.onTaskResizeAnimationListener = mock() mergeInterruptingTransition(mergeTarget = startTransition) defaultHandler.startAnimation( transition = requireNotNull(endTransition), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task1, ), startTransaction = mock(), finishTransaction = mock(), finishCallback = mock(), ) startDrag(defaultHandler, createTask()) verify(dragAnimator, times(2)).startAnimation() } private fun mergeInterruptingTransition(mergeTarget: IBinder) { defaultHandler.mergeAnimation( transition = mock<IBinder>(), info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = createTask()), startT = mock(), finishT = mock(), mergeTarget = mergeTarget, finishCallback = mock(), ) } private fun mergeAnimation( transition: IBinder = mock(), type: Int, mergeTarget: IBinder, task: RunningTaskInfo, ) { defaultHandler.mergeAnimation( transition = transition, info = createTransitionInfo(type = type, draggedTask = task), startT = mock(), finishT = mock(), mergeTarget = mergeTarget, finishCallback = mock(), ) } @Test fun getAnimationFraction_returnsFraction() { val fraction = Loading Loading @@ -785,6 +907,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { finishTransaction: SurfaceControl.Transaction = mock(), homeChange: TransitionInfo.Change? = createHomeChange(), transitionRootLeash: SurfaceControl = mock(), finishCallback: Transitions.TransitionFinishCallback = mock(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. Loading @@ -800,7 +923,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ), startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = {}, finishCallback = finishCallback, ) return transition } Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +4 −0 Original line number Diff line number Diff line Loading @@ -238,6 +238,10 @@ class DesktopTasksController( removeVisualIndicator() } override fun onTransitionInterrupted() { removeVisualIndicator() } private fun removeVisualIndicator() { visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +144 −6 Original line number Diff line number Diff line Loading @@ -24,8 +24,10 @@ import android.os.SystemClock import android.os.SystemProperties import android.os.UserHandle import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CLOSE import android.window.DesktopModeFlags import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo Loading Loading @@ -185,18 +187,29 @@ sealed class DragToDesktopTransitionHandler( */ fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? { if (!inProgress) { logV("finishDragToDesktop: not in progress, returning") // 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) { val state = requireTransitionState() if (state.startAborted) { logV("finishDragToDesktop: start was aborted, clearing state") // 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) if (state.startInterrupted) { logV("finishDragToDesktop: start was interrupted, returning") // We should only have interrupted the start transition after receiving a cancel/end // request, let that existing request play out and just return here. return null } state.endTransitionToken = transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) return state.endTransitionToken } /** Loading @@ -220,6 +233,11 @@ sealed class DragToDesktopTransitionHandler( clearState() return } if (state.startInterrupted) { // We should only have interrupted the start transition after receiving a cancel/end // request, let that existing request play out and just return here. return } state.cancelState = cancelState if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) { Loading @@ -227,7 +245,7 @@ sealed class DragToDesktopTransitionHandler( // 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() state.activeCancelAnimation = startCancelAnimation() } else if ( state.draggedTaskChange != null && (cancelState == CancelState.CANCEL_SPLIT_LEFT || Loading Loading @@ -255,7 +273,7 @@ sealed class DragToDesktopTransitionHandler( ) { if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { // TODO(b/388853233): add support for dragging split task to bubble startCancelAnimation() state.activeCancelAnimation = startCancelAnimation() } else { // Animation is handled by BubbleController val wct = WindowContainerTransaction() Loading Loading @@ -357,6 +375,19 @@ sealed class DragToDesktopTransitionHandler( ): Boolean { val state = requireTransitionState() if ( handleCancelOrExitAfterInterrupt( transition, info, startTransaction, finishTransaction, finishCallback, state, ) ) { return true } val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP && transition == state.startTransitionToken Loading Loading @@ -539,6 +570,58 @@ sealed class DragToDesktopTransitionHandler( } } private fun handleCancelOrExitAfterInterrupt( transition: IBinder, info: TransitionInfo, startTransaction: Transaction, finishTransaction: Transaction, finishCallback: Transitions.TransitionFinishCallback, state: TransitionState, ): Boolean { if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { return false } val isCancelDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && transition == state.cancelTransitionToken val isEndDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP && transition == state.endTransitionToken // We should only receive cancel or end transitions through startAnimation() if the // start transition was interrupted while a cancel- or end-transition had already // been requested. Finish the cancel/end transition to avoid having to deal with more // incoming transitions, and clear the state for the next start-drag transition. if (!isCancelDragToDesktop && !isEndDragToDesktop) { return false } if (!state.startInterrupted) { logW( "Not interrupted, but received startAnimation for cancel/end drag." + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" ) return false } logV( "startAnimation: interrupted -> " + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" ) if (isEndDragToDesktop) { setupEndDragToDesktop(info, startTransaction, finishTransaction) animateEndDragToDesktop(startTransaction = startTransaction, finishCallback) } else { // isCancelDragToDesktop // Similar to when we merge the cancel transition: ensure all tasks involved in the // cancel transition are shown, and finish the transition immediately. info.changes.forEach { change -> startTransaction.show(change.leash) finishTransaction.show(change.leash) } } startTransaction.apply() finishCallback.onTransitionFinished(/* wct= */ null) clearState() return true } /** * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is Loading Loading @@ -590,6 +673,7 @@ sealed class DragToDesktopTransitionHandler( ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { logV("mergeAnimation: end-transition, target=$mergeTarget") state.mergedEndTransition = true setupEndDragToDesktop( info, startTransaction = startT, Loading Loading @@ -617,6 +701,41 @@ sealed class DragToDesktopTransitionHandler( return } logW("unhandled merge transition: transitionInfo=$info") // Handle unknown incoming transitions by finishing the start transition. For now, only do // this if we've already requested a cancel- or end transition. If we've already merged the // end-transition, or if the end-transition is running on its own, then just wait until that // finishes instead. If we've merged the cancel-transition we've finished the // start-transition and won't reach this code. if ( mergeTarget == state.startTransitionToken && isCancelOrEndTransitionRequested(state) && !state.mergedEndTransition ) { interruptStartTransition(state) } } private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean = state.cancelTransitionToken != null || state.endTransitionToken != null private fun interruptStartTransition(state: TransitionState) { if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { return } logV("interruptStartTransition") state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) state.dragAnimator.cancelAnimator() state.activeCancelAnimation?.removeAllListeners() state.activeCancelAnimation?.cancel() state.activeCancelAnimation = null // Keep the transition state so we can deal with Cancel/End properly in #startAnimation. state.startInterrupted = true dragToDesktopStateListener?.onTransitionInterrupted() // Cancel CUJs here as they won't be accurate now that an incoming transition is playing. interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) LatencyTracker.getInstance(context) .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) } protected open fun setupEndDragToDesktop( Loading Loading @@ -783,7 +902,7 @@ sealed class DragToDesktopTransitionHandler( } ?: false } private fun startCancelAnimation() { private fun startCancelAnimation(): Animator { val state = requireTransitionState() val dragToDesktopAnimator = state.dragAnimator Loading @@ -800,7 +919,7 @@ sealed class DragToDesktopTransitionHandler( val dx = targetX - x val dy = targetY - y val tx: SurfaceControl.Transaction = transactionSupplier.get() ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) .apply { addUpdateListener { animator -> Loading @@ -818,6 +937,7 @@ sealed class DragToDesktopTransitionHandler( addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { state.activeCancelAnimation = null dragToDesktopStateListener?.onCancelToDesktopAnimationEnd() // Start the cancel transition to restore order. startCancelDragToDesktopTransition() Loading Loading @@ -910,10 +1030,16 @@ sealed class DragToDesktopTransitionHandler( val dragLayer: Int, ) /** Listener for various events happening during the DragToDesktop transition. */ interface DragToDesktopStateListener { /** Indicates that the animation into Desktop has started. */ fun onCommitToDesktopAnimationStart() /** Called when the animation to cancel the desktop-drag has finished. */ fun onCancelToDesktopAnimationEnd() /** Indicates that the drag-to-desktop transition has been interrupted. */ fun onTransitionInterrupted() } sealed class TransitionState { Loading @@ -930,6 +1056,10 @@ sealed class DragToDesktopTransitionHandler( abstract var cancelState: CancelState abstract var startAborted: Boolean abstract val visualIndicator: DesktopModeVisualIndicator? abstract var startInterrupted: Boolean abstract var endTransitionToken: IBinder? abstract var mergedEndTransition: Boolean abstract var activeCancelAnimation: Animator? data class FromFullscreen( override val draggedTaskId: Int, Loading @@ -945,6 +1075,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, override var startInterrupted: Boolean = false, override var endTransitionToken: IBinder? = null, override var mergedEndTransition: Boolean = false, override var activeCancelAnimation: Animator? = null, var otherRootChanges: MutableList<Change> = mutableListOf(), ) : TransitionState() Loading @@ -962,6 +1096,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, override var startInterrupted: Boolean = false, override var endTransitionToken: IBinder? = null, override var mergedEndTransition: Boolean = false, override var activeCancelAnimation: Animator? = null, var splitRootChange: Change? = null, var otherSplitTask: Int, ) : TransitionState() Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +133 −10 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder Loading @@ -34,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.CancelState import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT Loading @@ -56,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.MockitoSession import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.never Loading Loading @@ -118,6 +121,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .mockStatic(SystemProperties::class.java) .startMocking() whenever( transitions.startTransition( eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), /* wct= */ any(), eq(defaultHandler), ) ) .thenReturn(mock<IBinder>()) } @After Loading Loading @@ -679,17 +690,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val startTransition = startDrag(defaultHandler, task) val endTransition = mock<IBinder>() defaultHandler.onTaskResizeAnimationListener = mock() defaultHandler.mergeAnimation( mergeAnimation( transition = endTransition, info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), startT = mock<SurfaceControl.Transaction>(), finishT = mock<SurfaceControl.Transaction>(), task = task, mergeTarget = startTransition, finishCallback = mock<Transitions.TransitionFinishCallback>(), ) defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock()) Loading @@ -700,6 +705,123 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)) } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() { val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() defaultHandler.onTaskResizeAnimationListener = mock() val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback) mergeInterruptingTransition(mergeTarget = startTransition) verify(finishCallback, never()).onTransitionFinished(anyOrNull()) verify(dragAnimator, never()).cancelAnimator() } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() { val startDragFinishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() val startTransition = startDrag(defaultHandler, task, finishCallback = startDragFinishCallback) defaultHandler.onTaskResizeAnimationListener = mock() mergeAnimation( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, task = task, mergeTarget = startTransition, ) mergeInterruptingTransition(mergeTarget = startTransition) verify(startDragFinishCallback, never()).onTransitionFinished(anyOrNull()) } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun startEndAnimation_otherTransitionInterruptedStartAfterEndRequest_finishImmediately() { val task1 = createTask() val startTransition = startDrag(defaultHandler, task1) val endTransition = defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) val startTransaction = mock<SurfaceControl.Transaction>() val endDragFinishCallback = mock<Transitions.TransitionFinishCallback>() defaultHandler.onTaskResizeAnimationListener = mock() mergeInterruptingTransition(mergeTarget = startTransition) val didAnimate = defaultHandler.startAnimation( transition = requireNotNull(endTransition), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task1, ), startTransaction = startTransaction, finishTransaction = mock(), finishCallback = endDragFinishCallback, ) assertThat(didAnimate).isTrue() verify(startTransaction).apply() verify(endDragFinishCallback).onTransitionFinished(anyOrNull()) } @Test @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) fun startDrag_otherTransitionInterruptedStartAfterEndRequested_animatesDragWhenReady() { val task1 = createTask() val startTransition = startDrag(defaultHandler, task1) verify(dragAnimator).startAnimation() val endTransition = defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) defaultHandler.onTaskResizeAnimationListener = mock() mergeInterruptingTransition(mergeTarget = startTransition) defaultHandler.startAnimation( transition = requireNotNull(endTransition), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task1, ), startTransaction = mock(), finishTransaction = mock(), finishCallback = mock(), ) startDrag(defaultHandler, createTask()) verify(dragAnimator, times(2)).startAnimation() } private fun mergeInterruptingTransition(mergeTarget: IBinder) { defaultHandler.mergeAnimation( transition = mock<IBinder>(), info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = createTask()), startT = mock(), finishT = mock(), mergeTarget = mergeTarget, finishCallback = mock(), ) } private fun mergeAnimation( transition: IBinder = mock(), type: Int, mergeTarget: IBinder, task: RunningTaskInfo, ) { defaultHandler.mergeAnimation( transition = transition, info = createTransitionInfo(type = type, draggedTask = task), startT = mock(), finishT = mock(), mergeTarget = mergeTarget, finishCallback = mock(), ) } @Test fun getAnimationFraction_returnsFraction() { val fraction = Loading Loading @@ -785,6 +907,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { finishTransaction: SurfaceControl.Transaction = mock(), homeChange: TransitionInfo.Change? = createHomeChange(), transitionRootLeash: SurfaceControl = mock(), finishCallback: Transitions.TransitionFinishCallback = mock(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. Loading @@ -800,7 +923,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ), startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = {}, finishCallback = finishCallback, ) return transition } Loading