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

Commit d33ea49e authored by Daichi Hirono's avatar Daichi Hirono
Browse files

Implement animation for the move-to-display feature

Previously, the move-to-display operation used the default animation.
With the port_window_size_animation feature flag enabled, the default
handler applies a resize animation. This is because move-to-display
transactions change the task size in pixels to handle density changes
and preserve the DP size. Although this is technically a pixel size
change, the user perceives that the DP size remains constant.

To avoid the resize animation from the default transition handler, this
CL implements a dedicated transition handler for move-to-display
operations.

Bug: 376355478
Test: DesktopTasksControllerTest
Test: DesktopModeMoveToDisplayTransitionHandlerTest
Flag: com.android.window.flags.enable_move_to_next_display_shortcut
Change-Id: I95100ccdb0b5b96b0a1e9943e75fcd75ea140ccc
parent e6ee377e
Loading
Loading
Loading
Loading
+12 −2
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.os.Handler;
import android.os.UserManager;
import android.view.Choreographer;
import android.view.IWindowManager;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.DesktopModeFlags;

@@ -93,6 +94,7 @@ import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopModeMoveToDisplayTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger;
import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -772,7 +774,8 @@ public abstract class WMShellModule {
            DesksTransitionObserver desksTransitionObserver,
            UserProfileContexts userProfileContexts,
            DesktopModeCompatPolicy desktopModeCompatPolicy,
            DragToDisplayTransitionHandler dragToDisplayTransitionHandler) {
            DragToDisplayTransitionHandler dragToDisplayTransitionHandler,
            DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler) {
        return new DesktopTasksController(
                context,
                shellInit,
@@ -812,7 +815,8 @@ public abstract class WMShellModule {
                desksTransitionObserver,
                userProfileContexts,
                desktopModeCompatPolicy,
                dragToDisplayTransitionHandler);
                dragToDisplayTransitionHandler,
                moveToDisplayTransitionHandler);
    }

    @WMSingleton
@@ -948,6 +952,12 @@ public abstract class WMShellModule {
        return new DragToDisplayTransitionHandler();
    }

    @WMSingleton
    @Provides
    static DesktopModeMoveToDisplayTransitionHandler provideMoveToDisplayTransitionHandler() {
        return new DesktopModeMoveToDisplayTransitionHandler(new SurfaceControl.Transaction());
    }

    @WMSingleton
    @Provides
    static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
+96 −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.animation.Animator
import android.animation.ValueAnimator
import android.os.IBinder
import android.view.Choreographer
import android.view.SurfaceControl
import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.transition.Transitions
import kotlin.time.Duration.Companion.milliseconds

/**
 * Transition handler for moving a window to a different display.
 */
class DesktopModeMoveToDisplayTransitionHandler(
    private val animationTransaction: SurfaceControl.Transaction
) : Transitions.TransitionHandler {

    override fun handleRequest(
        transition: IBinder,
        request: TransitionRequestInfo,
    ): WindowContainerTransaction? = null

    override fun startAnimation(
        transition: IBinder,
        info: TransitionInfo,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction,
        finishCallback: Transitions.TransitionFinishCallback,
    ): Boolean {
        val change = info.changes.find { it.startDisplayId != it.endDisplayId } ?: return false
        ValueAnimator.ofFloat(0f, 1f)
            .apply {
                duration = ANIM_DURATION.inWholeMilliseconds
                interpolator = Interpolators.LINEAR
                addUpdateListener { animation ->
                    animationTransaction
                        .setAlpha(change.leash, animation.animatedValue as Float)
                        .setFrameTimeline(Choreographer.getInstance().vsyncId)
                        .apply()
                }
                addListener(
                    object : Animator.AnimatorListener {
                        override fun onAnimationStart(animation: Animator) {
                            val endBounds = change.endAbsBounds
                            startTransaction
                                .setPosition(
                                    change.leash,
                                    endBounds.left.toFloat(),
                                    endBounds.top.toFloat(),
                                )
                                .setWindowCrop(change.leash, endBounds.width(), endBounds.height())
                                .apply()
                        }

                        override fun onAnimationEnd(animation: Animator) {
                            finishTransaction.apply()
                            finishCallback.onTransitionFinished(null)
                        }

                        override fun onAnimationCancel(animation: Animator) {
                            finishTransaction.apply()
                            finishCallback.onTransitionFinished(null)
                        }

                        override fun onAnimationRepeat(animation: Animator) = Unit
                    }
                )
            }
            .start()
        return true
    }

    private companion object {
        val ANIM_DURATION = 100.milliseconds
    }
}
+5 −2
Original line number Diff line number Diff line
@@ -208,6 +208,7 @@ class DesktopTasksController(
    private val userProfileContexts: UserProfileContexts,
    private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
    private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
    private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler,
) :
    RemoteCallable<DesktopTasksController>,
    Transitions.TransitionHandler,
@@ -1228,7 +1229,8 @@ class DesktopTasksController(
                } else {
                    null
                }
            val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
            val transition =
                transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler)
            deactivationRunnable?.invoke(transition)
            return
        }
@@ -1296,7 +1298,8 @@ class DesktopTasksController(
            } else {
                null
            }
        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
        val transition =
            transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler)
        deactivationRunnable?.invoke(transition)
        activationRunnable?.invoke(transition)
    }
+87 −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.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.view.WindowManager
import android.window.TransitionInfo
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.util.StubTransaction
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@SmallTest
@RunWithLooper
@RunWith(AndroidTestingRunner::class)
class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() {
    private lateinit var handler: DesktopModeMoveToDisplayTransitionHandler

    @Before
    fun setUp() {
        handler = DesktopModeMoveToDisplayTransitionHandler(StubTransaction())
    }

    @Test
    fun handleRequest_returnsNull() {
        assertNull(handler.handleRequest(mock(), mock()))
    }

    @Test
    fun startAnimation_changeWithinDisplay_returnsFalse() {
        val animates =
            handler.startAnimation(
                transition = mock(),
                info =
                    TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
                        addChange(
                            TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 1) }
                        )
                    },
                startTransaction = StubTransaction(),
                finishTransaction = StubTransaction(),
                finishCallback = mock(),
            )

        assertFalse("Should not animate open transition", animates)
    }

    @Test
    fun startAnimation_changeMoveToDisplay_returnsTrue() {
        val animates =
            handler.startAnimation(
                transition = mock(),
                info =
                    TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
                        addChange(
                            TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 2) }
                        )
                    },
                startTransaction = StubTransaction(),
                finishTransaction = StubTransaction(),
                finishCallback = mock(),
            )

        assertTrue("Should animate display change transition", animates)
    }
}
+60 −18
Original line number Diff line number Diff line
@@ -263,6 +263,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
    @Mock private lateinit var packageManager: PackageManager
    @Mock private lateinit var mockDisplayContext: Context
    @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler
    @Mock
    private lateinit var moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler

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

    @After
@@ -2521,7 +2524,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
        controller.moveToNextDisplay(task.taskId)
        verifyWCTNotExecuted()
        verify(transitions, never()).startTransition(anyInt(), any(), anyOrNull())
    }

    @Test
@@ -2539,9 +2542,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        controller.moveToNextDisplay(task.taskId)

        val taskChange =
            getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find {
                it.container == task.token.asBinder() && it.isReparent
            }
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .hierarchyOps
                .find { it.container == task.token.asBinder() && it.isReparent }
        assertNotNull(taskChange)
        assertThat(taskChange.newParent).isEqualTo(secondDisplayArea.token.asBinder())
        assertThat(taskChange.toTop).isTrue()
@@ -2562,9 +2568,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        controller.moveToNextDisplay(task.taskId)

        val taskChange =
            getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find {
                it.container == task.token.asBinder() && it.isReparent
            }
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .hierarchyOps
                .find { it.container == task.token.asBinder() && it.isReparent }
        assertNotNull(taskChange)
        assertThat(taskChange.newParent).isEqualTo(defaultDisplayArea.token.asBinder())
        assertThat(taskChange.toTop).isTrue()
@@ -2589,7 +2598,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()

        controller.moveToNextDisplay(task.taskId)

        with(getLatestWct(type = TRANSIT_CHANGE)) {
        with(
            getLatestWct(
                type = TRANSIT_CHANGE,
                handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
            )
        ) {
            val wallpaperChange =
                hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }
            assertNotNull(wallpaperChange)
@@ -2615,9 +2629,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        controller.moveToNextDisplay(task.taskId)

        val wallpaperChange =
            getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { op ->
                op.container == wallpaperToken.asBinder()
            }
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .hierarchyOps
                .find { op -> op.container == wallpaperToken.asBinder() }
        assertNotNull(wallpaperChange)
        assertThat(wallpaperChange.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
    }
@@ -2649,7 +2666,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()

        controller.moveToNextDisplay(task.taskId)

        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
        val taskChange =
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .changes[task.token.asBinder()]
        assertNotNull(taskChange)
        // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
        // to the right margin and the ratio of the top margin to bottom margin are also
@@ -2686,7 +2708,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()

        controller.moveToNextDisplay(task.taskId)

        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
        val taskChange =
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .changes[task.token.asBinder()]
        assertNotNull(taskChange)
        assertThat(taskChange.configuration.windowConfiguration.bounds)
            .isEqualTo(Rect(960, 480, 1280, 720))
@@ -2717,7 +2744,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()

        controller.moveToNextDisplay(task.taskId)

        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
        val taskChange =
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .changes[task.token.asBinder()]
        assertNotNull(taskChange)
        // DP size is preserved. The window is centered in the destination display.
        assertThat(taskChange.configuration.windowConfiguration.bounds)
@@ -2755,7 +2787,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()

        controller.moveToNextDisplay(task.taskId)

        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
        val taskChange =
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .changes[task.token.asBinder()]
        assertNotNull(taskChange)
        assertThat(taskChange.configuration.windowConfiguration.bounds.left).isAtLeast(0)
        assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
@@ -2782,7 +2819,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        controller.moveToNextDisplay(task.taskId)

        val taskChange =
            getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find {
            getLatestWct(
                    type = TRANSIT_CHANGE,
                    handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java,
                )
                .hierarchyOps
                .find {
                    it.container == task.token.asBinder() && it.type == HIERARCHY_OP_TYPE_REORDER
                }
        assertNotNull(taskChange)