Refactor KeyGestureEventHandler out of DesktopTasksController into its
own class DesktopModeKeyGestureHandler.

Test: atest DesktopModeKeyGestureHandlerTest, DesktopTasksControllerTest
Flag: EXEMPT refactor only
Fixes: 375356887
Change-Id: I0b166080878c3b79d8b551080386cdf08010adcf
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 307e12e..00c1578 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
@@ -20,6 +20,8 @@
 import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
 import static android.window.DesktopModeFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS;
 
+import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.KeyguardManager;
@@ -76,6 +78,7 @@
 import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
 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.DesktopRepository;
 import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
@@ -758,8 +761,6 @@
                 dragToDesktopTransitionHandler,
                 desktopImmersiveController.get(),
                 desktopRepository,
-                desktopModeLoggerTransitionObserver,
-                launchAdjacentController,
                 recentsTransitionHandler,
                 multiInstanceHelper,
                 mainExecutor,
@@ -767,8 +768,6 @@
                 recentTasksController.orElse(null),
                 interactionJankMonitor,
                 mainHandler,
-                inputManager,
-                focusTransitionObserver,
                 desktopModeEventLogger,
                 desktopTilingDecorViewModel);
     }
@@ -883,6 +882,67 @@
 
     @WMSingleton
     @Provides
+    static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
+            Context context,
+            DesktopModeWindowDecorViewModel desktopModeWindowDecorViewModel,
+            Optional<DesktopTasksController> desktopTasksController,
+            InputManager inputManager,
+            ShellTaskOrganizer shellTaskOrganizer,
+            FocusTransitionObserver focusTransitionObserver) {
+        if (DesktopModeStatus.canEnterDesktopMode(context) && useKeyGestureEventHandler()
+                && Flags.enableMoveToNextDisplayShortcut()) {
+            return Optional.of(new DesktopModeKeyGestureHandler(context,
+                    desktopModeWindowDecorViewModel, desktopTasksController,
+                    inputManager, shellTaskOrganizer, focusTransitionObserver));
+        }
+        return Optional.empty();
+    }
+
+    @WMSingleton
+    @Provides
+    static DesktopModeWindowDecorViewModel provideDesktopModeWindowDecorViewModel(
+            Context context,
+            @ShellMainThread ShellExecutor shellExecutor,
+            @ShellMainThread Handler mainHandler,
+            Choreographer mainChoreographer,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            IWindowManager windowManager,
+            ShellTaskOrganizer taskOrganizer,
+            @DynamicOverride DesktopRepository desktopRepository,
+            DisplayController displayController,
+            ShellController shellController,
+            DisplayInsetsController displayInsetsController,
+            SyncTransactionQueue syncQueue,
+            Transitions transitions,
+            Optional<DesktopTasksController> desktopTasksController,
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            InteractionJankMonitor interactionJankMonitor,
+            AppToWebGenericLinksParser genericLinksParser,
+            AssistContentRequester assistContentRequester,
+            MultiInstanceHelper multiInstanceHelper,
+            Optional<DesktopTasksLimiter> desktopTasksLimiter,
+            AppHandleEducationController appHandleEducationController,
+            AppToWebEducationController appToWebEducationController,
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+            Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger
+    ) {
+        return new DesktopModeWindowDecorViewModel(context, shellExecutor, mainHandler,
+                mainChoreographer, bgExecutor, shellInit, shellCommandHandler, windowManager,
+                taskOrganizer, desktopRepository, displayController, shellController,
+                displayInsetsController, syncQueue, transitions, desktopTasksController,
+                rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser,
+                assistContentRequester, multiInstanceHelper, desktopTasksLimiter,
+                appHandleEducationController, appToWebEducationController,
+                windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
+                focusTransitionObserver, desktopModeEventLogger);
+    }
+
+    @WMSingleton
+    @Provides
     static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
             Transitions transitions,
             Optional<DesktopTasksLimiter> desktopTasksLimiter,
@@ -1234,7 +1294,8 @@
             DragAndDropController dragAndDropController,
             @NonNull LetterboxTransitionObserver letterboxTransitionObserver,
             Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional,
-            Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler) {
+            Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler,
+            Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler) {
         return new Object();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
new file mode 100644
index 0000000..db46f3c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.hardware.input.KeyGestureEvent
+import android.view.KeyEvent
+
+import android.hardware.input.InputManager
+import android.hardware.input.InputManager.KeyGestureEventHandler
+import android.os.IBinder
+import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
+import com.android.wm.shell.ShellTaskOrganizer
+import android.app.ActivityManager.RunningTaskInfo
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
+import com.android.internal.protolog.ProtoLog
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import com.android.wm.shell.transition.FocusTransitionObserver
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import java.util.Optional
+
+/**
+ * Handles key gesture events (keyboard shortcuts) in Desktop Mode.
+ */
+class DesktopModeKeyGestureHandler(
+    private val context: Context,
+    private val desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel,
+    private val desktopTasksController: Optional<DesktopTasksController>,
+    inputManager: InputManager,
+    private val shellTaskOrganizer: ShellTaskOrganizer,
+    private val focusTransitionObserver: FocusTransitionObserver,
+    ) : KeyGestureEventHandler {
+
+    init {
+        inputManager.registerKeyGestureEventHandler(this)
+    }
+
+    override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
+        if (!isKeyGestureSupported(event.keyGestureType) || !desktopTasksController.isPresent) {
+            return false
+        }
+        when (event.keyGestureType) {
+            KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> {
+                if (event.keycodes.contains(KeyEvent.KEYCODE_D) &&
+                    event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_META_ON)
+                ) {
+                    logV("Key gesture MOVE_TO_NEXT_DISPLAY is handled")
+                    getGloballyFocusedFreeformTask()?.let {
+                        desktopTasksController.get().moveToNextDisplay(
+                            it.taskId
+                        )
+                    }
+                    return true
+                }
+                return false
+            }
+            else -> return false
+        }
+    }
+
+    override fun isKeyGestureSupported(gestureType: Int): Boolean = when (gestureType) {
+        KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY
+            -> enableMoveToNextDisplayShortcut()
+        else -> false
+    }
+
+    //  TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it
+    //  will pick a wrong task when a user quickly perform other actions with keyboard shortcuts
+    //  after moveToNextDisplay, and move this to FocusTransitionObserver class.
+    private fun getGloballyFocusedFreeformTask(): RunningTaskInfo? =
+        shellTaskOrganizer.getRunningTasks().find { taskInfo ->
+            taskInfo.windowingMode == WINDOWING_MODE_FREEFORM &&
+                    focusTransitionObserver.hasGlobalFocus(taskInfo)
+        }
+
+    private fun logV(msg: String, vararg arguments: Any?) {
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
+    companion object {
+        private const val TAG = "DesktopModeKeyGestureHandler"
+    }
+}
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 4db0be5..cef5a30 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
@@ -35,9 +35,6 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.graphics.Region
-import android.hardware.input.InputManager
-import android.hardware.input.InputManager.KeyGestureEventHandler
-import android.hardware.input.KeyGestureEvent
 import android.os.Binder
 import android.os.Handler
 import android.os.IBinder
@@ -46,7 +43,6 @@
 import android.util.Size
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DragEvent
-import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.SurfaceControl.Transaction
@@ -66,7 +62,6 @@
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import androidx.annotation.BinderThread
-import com.android.hardware.input.Flags.useKeyGestureEventHandler
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
@@ -75,13 +70,11 @@
 import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.internal.protolog.ProtoLog
 import com.android.window.flags.Flags
-import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.common.ExternalInterfaceBinder
-import com.android.wm.shell.common.LaunchAdjacentController
 import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent
 import com.android.wm.shell.common.RemoteCallable
@@ -116,7 +109,6 @@
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.sysui.UserChangeListener
-import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
@@ -156,8 +148,6 @@
     private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
     private val desktopImmersiveController: DesktopImmersiveController,
     private val taskRepository: DesktopRepository,
-    private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
-    private val launchAdjacentController: LaunchAdjacentController,
     private val recentsTransitionHandler: RecentsTransitionHandler,
     private val multiInstanceHelper: MultiInstanceHelper,
     @ShellMainThread private val mainExecutor: ShellExecutor,
@@ -165,16 +155,13 @@
     private val recentTasksController: RecentTasksController?,
     private val interactionJankMonitor: InteractionJankMonitor,
     @ShellMainThread private val handler: Handler,
-    private val inputManager: InputManager,
-    private val focusTransitionObserver: FocusTransitionObserver,
     private val desktopModeEventLogger: DesktopModeEventLogger,
     private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel,
 ) :
     RemoteCallable<DesktopTasksController>,
     Transitions.TransitionHandler,
     DragAndDropController.DragAndDropListener,
-    UserChangeListener,
-    KeyGestureEventHandler {
+    UserChangeListener {
 
     private val desktopMode: DesktopModeImpl
     private var visualIndicator: DesktopModeVisualIndicator? = null
@@ -248,9 +235,6 @@
             }
         )
         dragAndDropController.addListener(this)
-        if (useKeyGestureEventHandler() && enableMoveToNextDisplayShortcut()) {
-            inputManager.registerKeyGestureEventHandler(this)
-        }
     }
 
     @VisibleForTesting
@@ -1835,26 +1819,12 @@
         getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) }
     }
 
-    /** Move the focused desktop task in given `displayId` to next display. */
-    fun moveFocusedTaskToNextDisplay(displayId: Int) {
-        getFocusedFreeformTask(displayId)?.let { moveToNextDisplay(it.taskId) }
-    }
-
     private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? {
         return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
             taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
         }
     }
 
-    // TODO(b/364154795): wait for the completion of moveToNextDisplay transition, otherwise it will
-    //  pick a wrong task when a user quickly perform other actions with keyboard shortcuts after
-    //  moveToNextDisplay.
-    private fun getGloballyFocusedFreeformTask(): RunningTaskInfo? =
-        shellTaskOrganizer.getRunningTasks().find { taskInfo ->
-            taskInfo.windowingMode == WINDOWING_MODE_FREEFORM &&
-                    focusTransitionObserver.hasGlobalFocus(taskInfo)
-        }
-
     /**
      * Requests a task be transitioned from desktop to split select. Applies needed windowing
      * changes if this transition is enabled.
@@ -2256,31 +2226,6 @@
         taskRepository.dump(pw, innerPrefix)
     }
 
-    override fun handleKeyGestureEvent(
-        event: KeyGestureEvent,
-        focusedToken: IBinder?
-    ): Boolean {
-        if (!isKeyGestureSupported(event.keyGestureType)) return false
-        when (event.keyGestureType) {
-            KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> {
-                if (event.keycodes.contains(KeyEvent.KEYCODE_D) &&
-                    event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_META_ON)) {
-                    logV("Key gesture MOVE_TO_NEXT_DISPLAY is handled")
-                    getGloballyFocusedFreeformTask()?.let { moveToNextDisplay(it.taskId) }
-                    return true
-                }
-                return false
-            }
-            else -> return false
-        }
-    }
-
-    override fun isKeyGestureSupported(gestureType: Int): Boolean = when (gestureType) {
-        KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY
-            -> enableMoveToNextDisplayShortcut()
-        else -> false
-    }
-
     /** The interface for calls from outside the shell, within the host process. */
     @ExternalThread
     private inner class DesktopModeImpl : DesktopMode {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
new file mode 100644
index 0000000..d18bf1f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 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.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.content.pm.ActivityInfo
+import android.graphics.Rect
+import android.hardware.input.InputManager
+import android.hardware.input.InputManager.KeyGestureEventHandler
+import android.hardware.input.KeyGestureEvent
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.KeyEvent
+import android.window.DisplayAreaInfo
+import androidx.test.filters.SmallTest
+import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS
+import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.transition.FocusTransitionObserver
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
+import java.util.Optional
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.quality.Strictness
+
+/**
+ * Test class for [DesktopModeKeyGestureHandler]
+ *
+ * Usage: atest WMShellUnitTests:DesktopModeKeyGestureHandlerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@ExperimentalCoroutinesApi
+@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
+
+    @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+    private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
+    private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
+    private val focusTransitionObserver = mock<FocusTransitionObserver>()
+    private val testExecutor = mock<ShellExecutor>()
+    private val inputManager = mock<InputManager>()
+    private val displayController = mock<DisplayController>()
+    private val displayLayout = mock<DisplayLayout>()
+    private val desktopModeWindowDecorViewModel = mock<DesktopModeWindowDecorViewModel>()
+    private val desktopTasksController = mock<DesktopTasksController>()
+
+    private lateinit var desktopModeKeyGestureHandler: DesktopModeKeyGestureHandler
+    private lateinit var keyGestureEventHandler: KeyGestureEventHandler
+    private lateinit var mockitoSession: StaticMockitoSession
+    private lateinit var testScope: CoroutineScope
+    private lateinit var shellInit: ShellInit
+
+    // Mock running tasks are registered here so we can get the list from mock shell task organizer
+    private val runningTasks = mutableListOf<RunningTaskInfo>()
+
+    @Before
+    fun setUp() {
+        Dispatchers.setMain(StandardTestDispatcher())
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .startMocking()
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+        testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+        shellInit = spy(ShellInit(testExecutor))
+
+        whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
+        whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+        }
+        val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
+
+        doAnswer {
+            keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler)
+            null
+        }.whenever(inputManager).registerKeyGestureEventHandler(any())
+        shellInit.init()
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+
+        runningTasks.clear()
+        testScope.cancel()
+    }
+
+    @Test
+    @EnableFlags(
+        FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS,
+        FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
+        FLAG_USE_KEY_GESTURE_EVENT_HANDLER
+    )
+    fun keyGestureMoveToNextDisplay_shouldMoveToNextDisplay() {
+        desktopModeKeyGestureHandler = DesktopModeKeyGestureHandler(
+            context,
+            desktopModeWindowDecorViewModel, Optional.of(desktopTasksController),
+            inputManager, shellTaskOrganizer, focusTransitionObserver
+        )
+        // Set up two display ids
+        whenever(rootTaskDisplayAreaOrganizer.displayIds)
+            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+        // Create a mock for the target display area: default display
+        val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .thenReturn(defaultDisplayArea)
+        // Setup a focused task on secondary display, which is expected to move to default display
+        val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
+        task.isFocused = true
+        whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task))
+        whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true)
+
+        val event = KeyGestureEvent.Builder()
+            .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY)
+            .setDisplayId(SECOND_DISPLAY)
+            .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D))
+            .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
+            .build()
+        val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+
+        assertThat(result).isTrue()
+        verify(desktopTasksController).moveToNextDisplay(task.taskId)
+    }
+
+    private fun setUpFreeformTask(
+        displayId: Int = DEFAULT_DISPLAY,
+        bounds: Rect? = null,
+    ): RunningTaskInfo {
+        val task = createFreeformTask(displayId, bounds)
+        val activityInfo = ActivityInfo()
+        task.topActivityInfo = activityInfo
+        whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        runningTasks.add(task)
+        return task
+    }
+
+    private companion object {
+        const val SECOND_DISPLAY = 2
+        val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
+    }
+}
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 2319716..1d7136b 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
@@ -39,9 +39,6 @@
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
-import android.hardware.input.InputManager
-import android.hardware.input.InputManager.KeyGestureEventHandler
-import android.hardware.input.KeyGestureEvent
 import android.os.Binder
 import android.os.Bundle
 import android.os.Handler
@@ -53,7 +50,6 @@
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DragEvent
 import android.view.Gravity
-import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.WindowInsets
@@ -75,18 +71,14 @@
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
 import android.window.WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID
 import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.ExtendedMockito.never
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.window.flags.Flags
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
-import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS
 import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP
-import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
 import com.android.wm.shell.MockToken
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -96,7 +88,6 @@
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayLayout
-import com.android.wm.shell.common.LaunchAdjacentController
 import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
@@ -124,7 +115,6 @@
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
-import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.TestRemoteTransition
 import com.android.wm.shell.transition.Transitions
@@ -208,12 +198,10 @@
   @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
   @Mock
   lateinit var mMockDesktopImmersiveController: DesktopImmersiveController
-  @Mock lateinit var launchAdjacentController: LaunchAdjacentController
   @Mock lateinit var splitScreenController: SplitScreenController
   @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
   @Mock lateinit var dragAndDropController: DragAndDropController
   @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
-  @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
   @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
   @Mock lateinit var recentTasksController: RecentTasksController
   @Mock
@@ -224,8 +212,6 @@
   @Mock private lateinit var mockHandler: Handler
   @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger
   @Mock lateinit var persistentRepository: DesktopPersistentRepository
-  @Mock private lateinit var mockInputManager: InputManager
-  @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
   @Mock lateinit var motionEvent: MotionEvent
   @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer
 
@@ -240,7 +226,6 @@
   private lateinit var desktopTasksLimiter: DesktopTasksLimiter
   private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
   private lateinit var testScope: CoroutineScope
-  private lateinit var keyGestureEventHandler: KeyGestureEventHandler
 
   private val shellExecutor = TestShellExecutor()
 
@@ -305,11 +290,6 @@
     controller.setSplitScreenController(splitScreenController)
     controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter
 
-    doAnswer {
-      keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler)
-      null
-    }.whenever(mockInputManager).registerKeyGestureEventHandler(any())
-
     shellInit.init()
 
     val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java)
@@ -343,8 +323,6 @@
         dragToDesktopTransitionHandler,
         mMockDesktopImmersiveController,
         taskRepository,
-        desktopModeLoggerTransitionObserver,
-        launchAdjacentController,
         recentsTransitionHandler,
         multiInstanceHelper,
         shellExecutor,
@@ -352,8 +330,6 @@
         recentTasksController,
         mockInteractionJankMonitor,
         mockHandler,
-        mockInputManager,
-        mockFocusTransitionObserver,
         desktopModeEventLogger,
         desktopTilingDecorViewModel,
       )
@@ -1558,44 +1534,6 @@
   }
 
   @Test
-  @EnableFlags(
-    FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS,
-    FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
-    FLAG_USE_KEY_GESTURE_EVENT_HANDLER
-  )
-  fun moveToNextDisplay_withKeyGesture() {
-    // Set up two display ids
-    whenever(rootTaskDisplayAreaOrganizer.displayIds)
-      .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
-    // Create a mock for the target display area: default display
-    val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
-    whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
-      .thenReturn(defaultDisplayArea)
-    // Setup a focused task on secondary display, which is expected to move to default display
-    val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
-    task.isFocused = true
-    whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task))
-    whenever(mockFocusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true)
-
-    val event = KeyGestureEvent.Builder()
-        .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY)
-        .setDisplayId(SECOND_DISPLAY)
-        .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D))
-        .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
-        .build()
-    val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
-
-    assertThat(result).isTrue()
-    with(getLatestWct(type = TRANSIT_CHANGE)) {
-      assertThat(hierarchyOps).hasSize(1)
-      assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
-      assertThat(hierarchyOps[0].isReparent).isTrue()
-      assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder())
-      assertThat(hierarchyOps[0].toTop).isTrue()
-    }
-  }
-
-  @Test
   fun getTaskWindowingMode() {
     val fullscreenTask = setUpFullscreenTask()
     val freeformTask = setUpFreeformTask()