diff options
5 files changed, 260 insertions, 22 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 6b7f311daa7c..2ded48a79d6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -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 @@ -950,6 +954,12 @@ public abstract class WMShellModule { @WMSingleton @Provides + static DesktopModeMoveToDisplayTransitionHandler provideMoveToDisplayTransitionHandler() { + return new DesktopModeMoveToDisplayTransitionHandler(new SurfaceControl.Transaction()); + } + + @WMSingleton + @Provides static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler( Context context, Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt new file mode 100644 index 000000000000..fbf170f13a40 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt @@ -0,0 +1,96 @@ +/* + * 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 + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 6c6606fe7202..301b79afd537 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -209,6 +209,7 @@ class DesktopTasksController( private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, + private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -1202,7 +1203,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 } @@ -1261,7 +1263,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) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt new file mode 100644 index 000000000000..fbc940663d19 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt @@ -0,0 +1,87 @@ +/* + * 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) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 8599bf4c81b4..8f499c959759 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -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,9 +2819,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) val taskChange = - getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { - it.container == task.token.asBinder() && it.type == HIERARCHY_OP_TYPE_REORDER - } + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .hierarchyOps + .find { + it.container == task.token.asBinder() && it.type == HIERARCHY_OP_TYPE_REORDER + } assertNotNull(taskChange) assertThat(taskChange.toTop).isTrue() assertThat(taskChange.includingParents()).isTrue() |