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

Commit 82185530 authored by Qijing Yao's avatar Qijing Yao
Browse files

Ensure drag indicators are cleared on transition abort

Overrides `onTransitionConsumed` in `WindowDragTransitionHandler` to
dispose of all multi-display drag indicators. This ensures that
indicator surfaces are properly cleaned up if a window drag transition
is aborted or consumed for any reason, preventing them from getting
stuck on screen.

Since the task ID is not available when this method is called, all
indicators are disposed of globally. This has a known, low-risk side
effect: if a user is dragging multiple windows simultaneously and one
transition aborts, indicators for all windows will be removed. This is
an acceptable trade-off given the rarity of the scenario.

Flag: com.android.window.flags.enable_window_drop_smooth_transition
Bug: 442613884
Test: manual test to ensure window drag working.
Test: atest WindowDragTransitionHandlerTest MultiDisplayDragMoveIndicatorControllerTest
Change-Id: I359797d156e3caff636e335add1a9d8c78af89ad
parent 55253a78
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -136,4 +136,16 @@ class MultiDisplayDragMoveIndicatorController(
                }
            }
    }

    /**
     * Disposes all of the indicator surfaces with the [transaction].
     */
    fun disposeAllIndicators(transaction: SurfaceControl.Transaction) {
        dragIndicators.values.forEach { innerIndicatorMap ->
            innerIndicatorMap.values.forEach { indicator ->
                indicator.dispose(transaction)
            }
        }
        dragIndicators.clear()
    }
}
+19 −0
Original line number Diff line number Diff line
@@ -66,4 +66,23 @@ class WindowDragTransitionHandler(
        finishCallback.onTransitionFinished(null)
        return true
    }

    override fun onTransitionConsumed(
        transition: IBinder,
        aborted: Boolean,
        finishTransaction: SurfaceControl.Transaction?,
    ) {
        // Cleans up drag indicators when a transition is consumed or aborted.
        // TODO: b/444066236 - Track pending transitions to clear indicators for the correct task.
        // The taskId is not available in this callback, so we must dispose of all indicators
        // across all displays. This has a known side effect: in the rare edge case that a user is
        // dragging multiple windows simultaneously and one drag transition aborts, the indicators
        // for all dragged windows will be removed. This is an acceptable trade-off due to the low
        // probability of this scenario.
        if (DesktopExperienceFlags.ENABLE_WINDOW_DROP_SMOOTH_TRANSITION.isTrue) {
            finishTransaction?.let {
                multiDisplayDragMoveIndicatorController.disposeAllIndicators(finishTransaction)
            }
        }
    }
}
+51 −0
Original line number Diff line number Diff line
@@ -56,10 +56,13 @@ class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() {
    private val shellDesktopState = FakeShellDesktopState(FakeDesktopState())
    private val indicatorSurface0 = mock<MultiDisplayDragMoveIndicatorSurface>()
    private val indicatorSurface1 = mock<MultiDisplayDragMoveIndicatorSurface>()
    private val indicatorSurface2 = mock<MultiDisplayDragMoveIndicatorSurface>()
    private val transaction = mock<SurfaceControl.Transaction>()
    private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>()
    private val taskInfo = mock<RunningTaskInfo>()
    private val taskInfo2 = mock<RunningTaskInfo>()
    private val taskLeash = mock<SurfaceControl>()
    private val taskLeash2 = mock<SurfaceControl>()
    private val displayContext0 = mock<Context>()
    private val displayContext1 = mock<Context>()
    private lateinit var spyDisplayLayout0: DisplayLayout
@@ -89,6 +92,7 @@ class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() {
        spyDisplayLayout1 = TestDisplay.DISPLAY_1.getSpyDisplayLayout(resources.resources)

        taskInfo.taskId = TASK_ID
        taskInfo2.taskId = TASK_ID_2
        whenever(displayController.getDisplayLayout(0)).thenReturn(spyDisplayLayout0)
        whenever(displayController.getDisplayLayout(1)).thenReturn(spyDisplayLayout1)
        whenever(displayController.getDisplayContext(0)).thenReturn(displayContext0)
@@ -97,6 +101,8 @@ class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() {
            .thenReturn(indicatorSurface0)
        whenever(indicatorSurfaceFactory.create(eq(displayContext1), eq(taskLeash)))
            .thenReturn(indicatorSurface1)
        whenever(indicatorSurfaceFactory.create(eq(displayContext0), eq(taskLeash2)))
            .thenReturn(indicatorSurface2)
        whenever(transactionSupplier.get()).thenReturn(transaction)
        shellDesktopState.canBeWindowDropTarget = true
    }
@@ -239,7 +245,52 @@ class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() {
        verify(indicatorSurface1).dispose(transaction)
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_WINDOW_DROP_SMOOTH_TRANSITION,
        Flags.FLAG_ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX,
    )
    fun disposeAllIndicators_verifyAllIndicatorsDisposed() {
        // Drag a first task to create indicators on two displays
        controller.onDragMove(
            RectF(100f, -100f, 200f, 200f), // intersect with display 0 and 1
            currentDisplayId = 1,
            startDisplayId = 0,
            taskLeash,
            taskInfo,
            displayIds = setOf(0, 1),
        ) {
            transaction
        }

        // Drag the second task to create an indicator on one display
        controller.onDragMove(
            RectF(150f, 150f, 250f, 250f), // intersect with display 0 only
            currentDisplayId = 0,
            startDisplayId = 0,
            taskLeash2,
            taskInfo2,
            displayIds = setOf(0, 1),
        ) {
            transaction
        }

        // Verify indicators for both tasks are created
        verify(indicatorSurfaceFactory).create(eq(displayContext0), eq(taskLeash))
        verify(indicatorSurfaceFactory).create(eq(displayContext1), eq(taskLeash))
        verify(indicatorSurfaceFactory).create(eq(displayContext0), eq(taskLeash2))

        // Dispose all indicators
        controller.disposeAllIndicators(transaction)

        // Verify indicators for both tasks are disposed
        verify(indicatorSurface0).dispose(transaction)
        verify(indicatorSurface1).dispose(transaction)
        verify(indicatorSurface2).dispose(transaction)
    }

    companion object {
        private const val TASK_ID = 10
        private const val TASK_ID_2 = 11
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -117,4 +117,12 @@ class WindowDragTransitionHandlerTest : ShellTestCase() {
        verify(mockMultiDisplayDragMoveIndicatorController)
            .onDragEnd(eq(10), eq(mockFinishTransaction))
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_WINDOW_DROP_SMOOTH_TRANSITION)
    fun onTransitionConsumed_disposesIndicators() {
        handler.onTransitionConsumed(mockTransition, false, mockFinishTransaction)
        verify(mockMultiDisplayDragMoveIndicatorController)
            .disposeAllIndicators(mockFinishTransaction)
    }
}