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

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

Specify transition handler for window drag across displays

`DefaultTransitionHandler` triggers `startBoundsChangeAnimation` when
`onConfigurationChanged` creates a `snapshot` due to width or height
changes. Dragging a task across displays with different DPIs can change
the task's actual width and height. This is expected, but the animation
is unnecessary and visually distracting since we have drag indicator
surfaces that follow the cursor, providing sufficient visual feedback
to the user.
This change specifies a transition handler that has no animation for
window drag across displays.

Bug: 390370556
Bug: 397354255
Test: Manual
Flag: com.android.window.flags.enable_connected_displays_window_drag
Change-Id: I79ba64abdd6f75f9ab65fd7c98df63707e2255db
parent 16233171
Loading
Loading
Loading
Loading
+11 −2
Original line number Diff line number Diff line
@@ -96,6 +96,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.DragToDisplayTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.OverviewToDesktopTransitionObserver;
@@ -765,7 +766,8 @@ public abstract class WMShellModule {
            DesksOrganizer desksOrganizer,
            DesksTransitionObserver desksTransitionObserver,
            UserProfileContexts userProfileContexts,
            DesktopModeCompatPolicy desktopModeCompatPolicy) {
            DesktopModeCompatPolicy desktopModeCompatPolicy,
            DragToDisplayTransitionHandler dragToDisplayTransitionHandler) {
        return new DesktopTasksController(
                context,
                shellInit,
@@ -803,7 +805,8 @@ public abstract class WMShellModule {
                desksOrganizer,
                desksTransitionObserver,
                userProfileContexts,
                desktopModeCompatPolicy);
                desktopModeCompatPolicy,
                dragToDisplayTransitionHandler);
    }

    @WMSingleton
@@ -927,6 +930,12 @@ public abstract class WMShellModule {
                        interactionJankMonitor, bubbleController);
    }

    @WMSingleton
    @Provides
    static DragToDisplayTransitionHandler provideDragToDisplayTransitionHandler() {
        return new DragToDisplayTransitionHandler();
    }

    @WMSingleton
    @Provides
    static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
+16 −16
Original line number Diff line number Diff line
@@ -202,6 +202,7 @@ class DesktopTasksController(
    private val desksTransitionObserver: DesksTransitionObserver,
    private val userProfileContexts: UserProfileContexts,
    private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
    private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
) :
    RemoteCallable<DesktopTasksController>,
    Transitions.TransitionHandler,
@@ -3021,25 +3022,24 @@ class DesktopTasksController(
                val wct = WindowContainerTransaction()
                wct.setBounds(taskInfo.token, destinationBounds)

                // TODO: b/362720497 - reparent to a specific desk within the target display.
                // Reparent task if it has been moved to a new display.
                if (Flags.enableConnectedDisplaysWindowDrag()) {
                val newDisplayId = motionEvent.getDisplayId()
                    if (newDisplayId != taskInfo.getDisplayId()) {
                        val displayAreaInfo =
                            rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
                        if (displayAreaInfo == null) {
                            logW(
                                "Task reparent cannot find DisplayAreaInfo for displayId=%d",
                                newDisplayId,
                            )
                val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
                val isCrossDisplayDrag =
                    Flags.enableConnectedDisplaysWindowDrag() &&
                        newDisplayId != taskInfo.getDisplayId() &&
                        displayAreaInfo != null
                val handler =
                    if (isCrossDisplayDrag) {
                        dragToDisplayTransitionHandler
                    } else {
                            wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
                        }
                        null
                    }
                if (isCrossDisplayDrag) {
                    // TODO: b/362720497 - reparent to a specific desk within the target display.
                    wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
                }

                transitions.startTransition(TRANSIT_CHANGE, wct, null)
                transitions.startTransition(TRANSIT_CHANGE, wct, handler)

                releaseVisualIndicator()
            }
+57 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.wm.shell.desktopmode

import android.os.IBinder
import android.view.SurfaceControl
import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import com.android.wm.shell.transition.Transitions

/** Handles the transition to drag a window to another display by dragging the caption. */
class DragToDisplayTransitionHandler : Transitions.TransitionHandler {
    override fun handleRequest(
        transition: IBinder,
        request: TransitionRequestInfo,
    ): WindowContainerTransaction? {
        return null
    }

    override fun startAnimation(
        transition: IBinder,
        info: TransitionInfo,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction,
        finishCallback: Transitions.TransitionFinishCallback,
    ): Boolean {
        for (change in info.changes) {
            val sc = change.leash
            val endBounds = change.endAbsBounds
            val endPosition = change.endRelOffset
            startTransaction
                .setWindowCrop(sc, endBounds.width(), endBounds.height())
                .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
            finishTransaction
                .setWindowCrop(sc, endBounds.width(), endBounds.height())
                .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
        }

        startTransaction.apply()
        finishCallback.onTransitionFinished(null)
        return true
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -261,6 +261,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
    @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver
    @Mock private lateinit var packageManager: PackageManager
    @Mock private lateinit var mockDisplayContext: Context
    @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler

    private lateinit var controller: DesktopTasksController
    private lateinit var shellInit: ShellInit
@@ -431,6 +432,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
            desksTransitionsObserver,
            userProfileContexts,
            desktopModeCompatPolicy,
            dragToDisplayTransitionHandler,
        )

    @After
@@ -4863,7 +4865,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
                Mockito.argThat { wct ->
                    return@argThat wct.hierarchyOps[0].isReparent
                },
                eq(null),
                eq(dragToDisplayTransitionHandler),
            )
    }

+101 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.wm.shell.desktopmode

import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
import android.view.SurfaceControl
import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import com.android.wm.shell.transition.Transitions
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

/**
 * Test class for {@link DragToDisplayTransitionHandler}
 *
 * Usage: atest WMShellUnitTests:DragToDisplayTransitionHandlerTest
 */
class DragToDisplayTransitionHandlerTest {
    private lateinit var handler: DragToDisplayTransitionHandler
    private val mockTransition: IBinder = mock()
    private val mockRequestInfo: TransitionRequestInfo = mock()
    private val mockTransitionInfo: TransitionInfo = mock()
    private val mockStartTransaction: SurfaceControl.Transaction = mock()
    private val mockFinishTransaction: SurfaceControl.Transaction = mock()
    private val mockFinishCallback: Transitions.TransitionFinishCallback = mock()

    @Before
    fun setUp() {
        handler = DragToDisplayTransitionHandler()
        whenever(mockStartTransaction.setWindowCrop(any(), any(), any()))
            .thenReturn(mockStartTransaction)
        whenever(mockFinishTransaction.setWindowCrop(any(), any(), any()))
            .thenReturn(mockFinishTransaction)
    }

    @Test
    fun handleRequest_anyRequest_returnsNull() {
        val result = handler.handleRequest(mockTransition, mockRequestInfo)
        assert(result == null)
    }

    @Test
    fun startAnimation_verifyTransformationsApplied() {
        val mockChange1 = mock<TransitionInfo.Change>()
        val leash1 = mock<SurfaceControl>()
        val endBounds1 = Rect(0, 0, 50, 50)
        val endPosition1 = Point(5, 5)

        whenever(mockChange1.leash).doReturn(leash1)
        whenever(mockChange1.endAbsBounds).doReturn(endBounds1)
        whenever(mockChange1.endRelOffset).doReturn(endPosition1)

        val mockChange2 = mock<TransitionInfo.Change>()
        val leash2 = mock<SurfaceControl>()
        val endBounds2 = Rect(100, 100, 200, 150)
        val endPosition2 = Point(15, 25)

        whenever(mockChange2.leash).doReturn(leash2)
        whenever(mockChange2.endAbsBounds).doReturn(endBounds2)
        whenever(mockChange2.endRelOffset).doReturn(endPosition2)

        whenever(mockTransitionInfo.changes).doReturn(listOf(mockChange1, mockChange2))

        handler.startAnimation(
            mockTransition,
            mockTransitionInfo,
            mockStartTransaction,
            mockFinishTransaction,
            mockFinishCallback,
        )

        verify(mockStartTransaction).setWindowCrop(leash1, endBounds1.width(), endBounds1.height())
        verify(mockStartTransaction)
            .setPosition(leash1, endPosition1.x.toFloat(), endPosition1.y.toFloat())
        verify(mockStartTransaction).setWindowCrop(leash2, endBounds2.width(), endBounds2.height())
        verify(mockStartTransaction)
            .setPosition(leash2, endPosition2.x.toFloat(), endPosition2.y.toFloat())
        verify(mockStartTransaction).apply()
        verify(mockFinishCallback).onTransitionFinished(null)
    }
}