Extend close window shortcut to split-screen tasks

This change extends the Action+Ctrl+W keyboard shortcut to close the
focused task in a split-screen pair. This builds upon the existing
functionality that allows closing desktop and fullscreen windows.

The feature is controlled by the
`close_fullscreen_and_splitscreen_keyboard_shortcut` flag.

Bug: 441147192
Test: DesktopModeKeyGestureHandlerTest, DesktopModeWindowDecorViewModelTests
Flag: com.android.window.flags.close_fullscreen_and_splitscreen_keyboard_shortcut
Change-Id: Ibb9758db29714c02fdc785a5b93b2d9f27a56b52
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 9905435..3f6a7d2 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
@@ -1259,7 +1259,8 @@
             FocusTransitionObserver focusTransitionObserver,
             @ShellMainThread ShellExecutor mainExecutor,
             DisplayController displayController,
-            DesktopState desktopState) {
+            DesktopState desktopState,
+            Optional<SplitScreenController> splitScreenController) {
         if (desktopState.canEnterDesktopMode()
                 && (DesktopExperienceFlags.ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT.isTrue()
                 || DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue())) {
@@ -1267,7 +1268,7 @@
                     desktopModeWindowDecorViewModel, desktopTasksController,
                     desktopUserRepositories,
                     inputManager, shellTaskOrganizer, focusTransitionObserver,
-                    mainExecutor, displayController, desktopState));
+                    mainExecutor, displayController, desktopState, splitScreenController));
         }
         return Optional.empty();
     }
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
index 8c2bab2..0f60b73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -19,6 +19,7 @@
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.content.Context
 import android.hardware.input.InputManager
 import android.hardware.input.InputManager.KeyGestureEventHandler
@@ -37,6 +38,7 @@
 import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.desktopmode.DesktopState
+import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
 import java.util.Optional
@@ -53,6 +55,7 @@
     @ShellMainThread private val mainExecutor: ShellExecutor,
     private val displayController: DisplayController,
     private val desktopState: DesktopState,
+    private val splitScreenController: Optional<SplitScreenController>,
 ) : KeyGestureEventHandler {
 
     init {
@@ -340,8 +343,21 @@
                     null
                 }
             }
+            2 -> {
+                val task = DesktopTasksController.getSplitFocusedTask(tasks[0], tasks[1])
+                if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+                    logV("getGloballyFocusedTaskToClose: Found split screen task: %d", task.taskId)
+                    task
+                } else {
+                    logW(
+                        "getGloballyFocusedTaskToClose: Ignored focused pair non-split-screen " +
+                            "tasks."
+                    )
+                    null
+                }
+            }
             else -> {
-                logW("getGloballyFocusedTaskToClose: Ignored focused 2+ tasks.")
+                logW("getGloballyFocusedTaskToClose: Ignored focused 3+ tasks.")
                 null
             }
         }
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 28c936a..448a24a 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
@@ -526,10 +526,6 @@
         }
     }
 
-    /** Returns child task from two focused tasks in split screen mode. */
-    private fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) =
-        if (task1.taskId == task2.parentTaskId) task2 else task1
-
     /** Moves a desktop task into fullscreen mode. */
     private fun moveDesktopTaskToFullscreen(
         task: RunningTaskInfo,
@@ -6604,6 +6600,10 @@
                     UnminimizeReason.TASKBAR_MANAGE_WINDOW
             }
 
+        /** Returns child task from two focused tasks in split screen mode. */
+        fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) =
+            if (task1.taskId == task2.parentTaskId) task2 else task1
+
         @JvmField
         /**
          * A placeholder for a synthetic transition that isn't backed by a true system transition.
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
index e8af353..d65383f 100644
--- 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
@@ -52,11 +52,13 @@
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSplitScreenTask
 import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
 import com.android.wm.shell.desktopmode.data.DesktopRepository
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.shared.desktopmode.FakeDesktopConfig
 import com.android.wm.shell.shared.desktopmode.FakeDesktopState
+import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.FocusTransitionObserver
@@ -104,6 +106,7 @@
     private val desktopTasksController = mock<DesktopTasksController>()
     private val desktopState = FakeDesktopState()
     private val shellController = mock<ShellController>()
+    private val splitScreenController = mock<SplitScreenController>()
 
     private lateinit var desktopModeKeyGestureHandler: DesktopModeKeyGestureHandler
     private lateinit var keyGestureEventHandler: KeyGestureEventHandler
@@ -174,6 +177,7 @@
                 testExecutor,
                 displayController,
                 desktopState,
+                Optional.of(splitScreenController),
             )
     }
 
@@ -414,6 +418,34 @@
     }
 
     @Test
+    @EnableFlags(FLAG_CLOSE_FULLSCREEN_AND_SPLITSCREEN_KEYBOARD_SHORTCUT)
+    fun keyGestureQuitFocusedDesktopTask_shouldQuitSplitScreenTask() {
+        // Setup a focused split screen task
+        val task = setUpSplitScreenTask()
+        val splitRoot = setUpFullscreenTask()
+        task.parentTaskId = splitRoot.taskId
+        whenever(focusTransitionObserver.globallyFocusedDisplayId).thenReturn(task.displayId)
+        whenever(
+                desktopTasksController.getFocusedNonDesktopTasks(task.displayId, repository.userId)
+            )
+            .thenReturn(listOf(splitRoot, task))
+        whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
+
+        // Create and handle the key gesture event
+        val event =
+            KeyGestureEvent.Builder()
+                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_QUIT_FOCUSED_DESKTOP_TASK)
+                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_Q))
+                .setModifierState(KeyEvent.META_META_ON)
+                .build()
+        keyGestureEventHandler.handleKeyGestureEvent(event, null)
+        testExecutor.flushAll()
+
+        // Verify closeTask is called
+        verify(desktopModeWindowDecorViewModel).closeTask(task)
+    }
+
+    @Test
     fun keyGestureSwitchToPreviousDesk_activatesDesk() {
         val displayId = 2
         whenever(focusTransitionObserver.globallyFocusedDisplayId).thenReturn(displayId)
@@ -492,6 +524,13 @@
         return task
     }
 
+    private fun setUpSplitScreenTask(): RunningTaskInfo {
+        val task = createSplitScreenTask()
+        whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        runningTasks.add(task)
+        return task
+    }
+
     private companion object {
         private const val DEFAULT_USER_ID = 0
         const val SECOND_DISPLAY = 2
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 2cba2fe..3bea35a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -78,6 +78,8 @@
 import com.android.wm.shell.recents.RecentsTransitionStateListener
 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
+import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.util.StubTransaction
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DefaultWindowDecorationActions
@@ -376,6 +378,30 @@
     }
 
     @Test
+    fun testCloseTask_splitScreen_movesOtherToFullscreen() {
+        desktopModeWindowDecorViewModel.setFreeformTaskTransitionStarter(
+            mockFreeformTaskTransitionStarter
+        )
+        val decor = createOpenTaskDecoration(windowingMode = WINDOWING_MODE_MULTI_WINDOW)
+        val otherTask = createTask(windowingMode = WINDOWING_MODE_MULTI_WINDOW)
+
+        whenever(mockSplitScreenController.isTaskInSplitScreen(decor.taskInfo.taskId))
+            .thenReturn(true)
+        whenever(mockSplitScreenController.getSplitPosition(decor.taskInfo.taskId))
+            .thenReturn(SPLIT_POSITION_TOP_OR_LEFT)
+        whenever(mockSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT))
+            .thenReturn(otherTask)
+
+        desktopModeWindowDecorViewModel.closeTask(decor.taskInfo)
+
+        verify(mockSplitScreenController)
+            .moveTaskToFullscreen(
+                eq(otherTask.taskId),
+                eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE),
+            )
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_MINIMIZE_BUTTON)
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_HEADER_STATE_CHANGE_ANNOUNCEMENTS)
     fun testMinimizeButtonInFreeform_minimizeWindow() {