Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5aa2cd3e authored by Gustav Sennton's avatar Gustav Sennton
Browse files

[2/N] Interrupt DragToDesktop before bookend request -> move to Home

If the drag-to-desktop transition is interrupted before a cancel or end
transition has been requested:
- return to the Home screen
- cancel the current app handle drag

Test: launch a half-transparent activity while dragging a window to
desktop
Bug: 397135730
Bug: 397497184
Flag: com.android.window.flags.enable_drag_to_desktop_incoming_transitions_bugfix

Change-Id: Id0ea7f57fc1992faf66d096ceadea991bfec74e6
parent 5c715c15
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -32,8 +32,6 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
@@ -652,6 +650,7 @@ class DesktopTasksController(
        taskInfo: RunningTaskInfo,
        dragToDesktopValueAnimator: MoveToDesktopAnimator,
        taskSurface: SurfaceControl,
        dragInterruptedCallback: Runnable,
    ) {
        logV("startDragToDesktop taskId=%d", taskInfo.taskId)
        val jankConfigBuilder =
@@ -667,6 +666,7 @@ class DesktopTasksController(
            taskInfo,
            dragToDesktopValueAnimator,
            visualIndicator,
            dragInterruptedCallback,
        )
    }

+76 −12
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ package com.android.wm.shell.desktopmode

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.RectEvaluator
import android.animation.ValueAnimator
import android.app.ActivityManager.RunningTaskInfo
@@ -23,6 +24,7 @@ import android.os.IBinder
import android.os.SystemClock
import android.os.SystemProperties
import android.os.UserHandle
import android.view.Choreographer
import android.view.SurfaceControl
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CLOSE
@@ -48,6 +50,7 @@ import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKT
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.shared.animation.PhysicsAnimator
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
@@ -122,6 +125,7 @@ sealed class DragToDesktopTransitionHandler(
        taskInfo: RunningTaskInfo,
        dragToDesktopAnimator: MoveToDesktopAnimator,
        visualIndicator: DesktopModeVisualIndicator?,
        dragCancelCallback: Runnable,
    ) {
        if (inProgress) {
            logV("Drag to desktop transition already in progress.")
@@ -168,6 +172,7 @@ sealed class DragToDesktopTransitionHandler(
                    startTransitionToken = startTransitionToken,
                    otherSplitTask = otherTask,
                    visualIndicator = visualIndicator,
                    dragCancelCallback = dragCancelCallback,
                )
            } else {
                TransitionState.FromFullscreen(
@@ -175,6 +180,7 @@ sealed class DragToDesktopTransitionHandler(
                    dragAnimator = dragToDesktopAnimator,
                    startTransitionToken = startTransitionToken,
                    visualIndicator = visualIndicator,
                    dragCancelCallback = dragCancelCallback,
                )
            }
    }
@@ -203,8 +209,9 @@ sealed class DragToDesktopTransitionHandler(
        }
        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.
            // If start was interrupted we've either already requested a cancel/end transition - so
            // we should let that request play out, or we're cancelling the drag-to-desktop
            // transition altogether, so just return here.
            return null
        }
        state.endTransitionToken =
@@ -221,6 +228,7 @@ sealed class DragToDesktopTransitionHandler(
     */
    fun cancelDragToDesktopTransition(cancelState: CancelState) {
        if (!inProgress) {
            logV("cancelDragToDesktop: not in progress, returning")
            // 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.
@@ -228,14 +236,17 @@ sealed class DragToDesktopTransitionHandler(
        }
        val state = requireTransitionState()
        if (state.startAborted) {
            logV("cancelDragToDesktop: start was aborted, clearing state")
            // 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
        }
        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.
            logV("cancelDragToDesktop: start was interrupted, returning")
            // If start was interrupted we've either already requested a cancel/end transition - so
            // we should let that request play out, or we're cancelling the drag-to-desktop
            // transition altogether, so just return here.
            return
        }
        state.cancelState = cancelState
@@ -706,11 +717,7 @@ sealed class DragToDesktopTransitionHandler(
        // 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
        ) {
        if (mergeTarget == state.startTransitionToken && !state.mergedEndTransition) {
            interruptStartTransition(state)
        }
    }
@@ -722,9 +729,23 @@ sealed class DragToDesktopTransitionHandler(
        if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
            return
        }
        logV("interruptStartTransition")
        if (isCancelOrEndTransitionRequested(state)) {
            logV("interruptStartTransition, bookend requested -> finish start transition")
            // Finish the start-drag transition, we will finish the overall transition properly when
            // receiving #startAnimation for Cancel/End.
            state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
            state.dragAnimator.cancelAnimator()
        } else {
            logV("interruptStartTransition, bookend not requested -> animate to Home")
            // Animate to Home, and then finish the start-drag transition. Since there is no other
            // (end/cancel) transition requested that will be the end of the overall transition.
            state.dragAnimator.cancelAnimator()
            state.dragCancelCallback?.run()
            createInterruptToHomeAnimator(transactionSupplier.get(), state) {
                state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
                clearState()
            }
        }
        state.activeCancelAnimation?.removeAllListeners()
        state.activeCancelAnimation?.cancel()
        state.activeCancelAnimation = null
@@ -738,6 +759,46 @@ sealed class DragToDesktopTransitionHandler(
            .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
    }

    private fun createInterruptToHomeAnimator(
        transaction: Transaction,
        state: TransitionState,
        endCallback: Runnable,
    ) {
        val homeLeash = state.homeChange?.leash ?: error("Expected home leash to be non-null")
        val draggedTaskLeash =
            state.draggedTaskChange?.leash ?: error("Expected dragged leash to be non-null")
        val homeAnimator = createInterruptAlphaAnimator(transaction, homeLeash, toShow = true)
        val draggedTaskAnimator =
            createInterruptAlphaAnimator(transaction, draggedTaskLeash, toShow = false)
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(homeAnimator, draggedTaskAnimator)
        animatorSet.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    endCallback.run()
                }
            }
        )
        animatorSet.start()
    }

    private fun createInterruptAlphaAnimator(
        transaction: Transaction,
        leash: SurfaceControl,
        toShow: Boolean,
    ) =
        ValueAnimator.ofFloat(if (toShow) 0f else 1f, if (toShow) 1f else 0f).apply {
            transaction.show(leash)
            duration = DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS
            interpolator = Interpolators.LINEAR
            addUpdateListener { animation ->
                transaction
                    .setAlpha(leash, animation.animatedValue as Float)
                    .setFrameTimeline(Choreographer.getInstance().vsyncId)
                    .apply()
            }
        }

    protected open fun setupEndDragToDesktop(
        info: TransitionInfo,
        startTransaction: SurfaceControl.Transaction,
@@ -1060,6 +1121,7 @@ sealed class DragToDesktopTransitionHandler(
        abstract var endTransitionToken: IBinder?
        abstract var mergedEndTransition: Boolean
        abstract var activeCancelAnimation: Animator?
        abstract var dragCancelCallback: Runnable?

        data class FromFullscreen(
            override val draggedTaskId: Int,
@@ -1079,6 +1141,7 @@ sealed class DragToDesktopTransitionHandler(
            override var endTransitionToken: IBinder? = null,
            override var mergedEndTransition: Boolean = false,
            override var activeCancelAnimation: Animator? = null,
            override var dragCancelCallback: Runnable? = null,
            var otherRootChanges: MutableList<Change> = mutableListOf(),
        ) : TransitionState()

@@ -1100,6 +1163,7 @@ sealed class DragToDesktopTransitionHandler(
            override var endTransitionToken: IBinder? = null,
            override var mergedEndTransition: Boolean = false,
            override var activeCancelAnimation: Animator? = null,
            override var dragCancelCallback: Runnable? = null,
            var splitRootChange: Change? = null,
            var otherSplitTask: Int,
        ) : TransitionState()
+24 −5
Original line number Diff line number Diff line
@@ -979,6 +979,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
        private boolean mIsCustomHeaderGesture;
        private boolean mIsResizeGesture;
        private boolean mIsDragging;
        private boolean mDragInterrupted;
        private boolean mLongClickDisabled;
        private int mDragPointerId = -1;
        private MotionEvent mMotionEvent;
@@ -1216,7 +1217,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                View v, MotionEvent e) {
            final int id = v.getId();
            if (id == R.id.caption_handle) {
                handleCaptionThroughStatusBar(e, decoration);
                handleCaptionThroughStatusBar(e, decoration,
                        /* interruptDragCallback= */
                        () -> {
                            mDragInterrupted = true;
                            setIsDragging(decoration, /* isDragging= */ false);
                        });
                final boolean wasDragging = mIsDragging;
                updateDragStatus(decoration, e);
                final boolean upOrCancel = e.getActionMasked() == ACTION_UP
@@ -1333,11 +1339,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL: {
                    mDragInterrupted = false;
                    setIsDragging(decor, false /* isDragging */);
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    if (!mDragInterrupted) {
                        setIsDragging(decor, true /* isDragging */);
                    }
                    break;
                }
            }
@@ -1458,7 +1467,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
            if (!mInImmersiveMode && (relevantDecor == null
                    || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM
                    || mTransitionDragActive)) {
                handleCaptionThroughStatusBar(ev, relevantDecor);
                handleCaptionThroughStatusBar(ev, relevantDecor,
                        /* interruptDragCallback= */ () -> {});
            }
        }
        handleEventOutsideCaption(ev, relevantDecor);
@@ -1498,7 +1508,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
     * Turn on desktop mode if handle is dragged below status bar.
     */
    private void handleCaptionThroughStatusBar(MotionEvent ev,
            DesktopModeWindowDecoration relevantDecor) {
            DesktopModeWindowDecoration relevantDecor, Runnable interruptDragCallback) {
        if (relevantDecor == null) {
            if (ev.getActionMasked() == ACTION_UP) {
                mMoveToDesktopAnimator = null;
@@ -1599,7 +1609,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                                    mContext, mDragToDesktopAnimationStartBounds,
                                    relevantDecor.mTaskInfo, relevantDecor.mTaskSurface);
                            mDesktopTasksController.startDragToDesktop(relevantDecor.mTaskInfo,
                                    mMoveToDesktopAnimator, relevantDecor.mTaskSurface);
                                    mMoveToDesktopAnimator, relevantDecor.mTaskSurface,
                                    /* dragInterruptedCallback= */ () -> {
                                        // Don't call into DesktopTasksController to cancel the
                                        // transition here - the transition handler already handles
                                        // that (including removing the visual indicator).
                                        mTransitionDragActive = false;
                                        mMoveToDesktopAnimator = null;
                                        relevantDecor.handleDragInterrupted();
                                        interruptDragCallback.run();
                                    });
                        }
                    }
                    if (mMoveToDesktopAnimator != null) {
+11 −0
Original line number Diff line number Diff line
@@ -1682,6 +1682,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
        }
    }

    /**
     * Indicates that an app handle drag has been interrupted, this can happen e.g. if we receive an
     * unknown transition during the drag-to-desktop transition.
     */
    void handleDragInterrupted() {
        if (mResult.mRootView == null) return;
        final View handle = mResult.mRootView.findViewById(R.id.caption_handle);
        handle.setHovered(false);
        handle.setPressed(false);
    }

    private boolean pointInView(View v, float x, float y) {
        return v != null && v.getLeft() <= x && v.getRight() >= x
                && v.getTop() <= y && v.getBottom() >= y;
+93 −7
Original line number Diff line number Diff line
@@ -54,7 +54,9 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.MockitoSession
@@ -85,8 +87,17 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
    @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories
    @Mock private lateinit var bubbleController: BubbleController
    @Mock private lateinit var visualIndicator: DesktopModeVisualIndicator

    private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
    @Mock private lateinit var dragCancelCallback: Runnable
    @Mock
    private lateinit var dragToDesktopStateListener:
        DragToDesktopTransitionHandler.DragToDesktopStateListener

    private val transactionSupplier = Supplier {
        val transaction = mock<SurfaceControl.Transaction>()
        whenever(transaction.setAlpha(any(), anyFloat())).thenReturn(transaction)
        whenever(transaction.setFrameTimeline(anyLong())).thenReturn(transaction)
        transaction
    }

    private lateinit var defaultHandler: DragToDesktopTransitionHandler
    private lateinit var springHandler: SpringDragToDesktopTransitionHandler
@@ -104,7 +115,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
                    Optional.of(bubbleController),
                    transactionSupplier,
                )
                .apply { setSplitScreenController(splitScreenController) }
                .apply {
                    setSplitScreenController(splitScreenController)
                    dragToDesktopStateListener =
                        this@DragToDesktopTransitionHandlerTest.dragToDesktopStateListener
                }
        springHandler =
            SpringDragToDesktopTransitionHandler(
                    context,
@@ -115,7 +130,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
                    Optional.of(bubbleController),
                    transactionSupplier,
                )
                .apply { setSplitScreenController(splitScreenController) }
                .apply {
                    setSplitScreenController(splitScreenController)
                    dragToDesktopStateListener =
                        this@DragToDesktopTransitionHandlerTest.dragToDesktopStateListener
                }
        mockitoSession =
            ExtendedMockito.mockitoSession()
                .strictness(Strictness.LENIENT)
@@ -706,8 +725,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
    }

    @Test
    @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
    fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() {
    @DisableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
    fun mergeOtherTransition_flagDisabled_cancelAndEndNotYetRequested_doesNotInterruptStartDrag() {
        val finishCallback = mock<Transitions.TransitionFinishCallback>()
        val task = createTask()
        defaultHandler.onTaskResizeAnimationListener = mock()
@@ -719,6 +738,39 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
        verify(dragAnimator, never()).cancelAnimator()
    }

    @Test
    @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
    fun mergeOtherTransition_cancelAndEndNotYetRequested_interruptsStartDrag() {
        val finishCallback = mock<Transitions.TransitionFinishCallback>()
        val task = createTask()
        defaultHandler.onTaskResizeAnimationListener = mock()
        val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback)

        mergeInterruptingTransition(mergeTarget = startTransition)

        verify(dragAnimator).cancelAnimator()
        verify(dragCancelCallback).run()
        verify(dragToDesktopStateListener).onTransitionInterrupted()
        assertThat(defaultHandler.inProgress).isTrue()
        // Doesn't finish start transition yet
        verify(finishCallback, never()).onTransitionFinished(/* wct= */ anyOrNull())
    }

    @Test
    @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
    fun mergeOtherTransition_cancelAndEndNotYetRequested_finishesStartAfterAnimation() {
        val finishCallback = mock<Transitions.TransitionFinishCallback>()
        val task = createTask()
        defaultHandler.onTaskResizeAnimationListener = mock()
        val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback)

        mergeInterruptingTransition(mergeTarget = startTransition)
        mAnimatorTestRule.advanceTimeBy(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)

        verify(finishCallback).onTransitionFinished(/* wct= */ anyOrNull())
        assertThat(defaultHandler.inProgress).isFalse()
    }

    @Test
    @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
    fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() {
@@ -795,6 +847,35 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
        verify(dragAnimator, times(2)).startAnimation()
    }

    @Test
    @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
    fun startCancelAnimation_otherTransitionInterruptingAfterCancelRequest_finishImmediately() {
        val task1 = createTask()
        val startTransition = startDrag(defaultHandler, task1)
        val cancelTransition =
            cancelDragToDesktopTransition(defaultHandler, CancelState.STANDARD_CANCEL)
        mergeInterruptingTransition(mergeTarget = startTransition)
        val cancelFinishCallback = mock<Transitions.TransitionFinishCallback>()
        val startTransaction = mock<SurfaceControl.Transaction>()

        val didAnimate =
            defaultHandler.startAnimation(
                transition = requireNotNull(cancelTransition),
                info =
                    createTransitionInfo(
                        type = TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP,
                        draggedTask = task1,
                    ),
                startTransaction = startTransaction,
                finishTransaction = mock(),
                finishCallback = cancelFinishCallback,
            )

        assertThat(didAnimate).isTrue()
        verify(startTransaction).apply()
        verify(cancelFinishCallback).onTransitionFinished(/* wct= */ anyOrNull())
    }

    private fun mergeInterruptingTransition(mergeTarget: IBinder) {
        defaultHandler.mergeAnimation(
            transition = mock<IBinder>(),
@@ -942,7 +1023,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
                )
            )
            .thenReturn(token)
        handler.startDragToDesktopTransition(task, dragAnimator, visualIndicator)
        handler.startDragToDesktopTransition(
            task,
            dragAnimator,
            visualIndicator,
            dragCancelCallback,
        )
        return token
    }