summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt96
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt87
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt78
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()