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

Commit 82ffdfea authored by Vania Desmonda's avatar Vania Desmonda
Browse files

Refactor KeyGestureEventHandler out of DesktopTasksController into its

own class DesktopModeKeyGestureHandler.

Test: atest DesktopModeKeyGestureHandlerTest, DesktopTasksControllerTest
Flag: EXEMPT refactor only
Fixes: 375356887
Change-Id: I0b166080878c3b79d8b551080386cdf08010adcf
parent c153de07
Loading
Loading
Loading
Loading
+66 −5
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRA
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.DesktopImmersiveController;
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 @@ public abstract class WMShellModule {
                dragToDesktopTransitionHandler,
                desktopImmersiveController.get(),
                desktopRepository,
                desktopModeLoggerTransitionObserver,
                launchAdjacentController,
                recentsTransitionHandler,
                multiInstanceHelper,
                mainExecutor,
@@ -767,8 +768,6 @@ public abstract class WMShellModule {
                recentTasksController.orElse(null),
                interactionJankMonitor,
                mainHandler,
                inputManager,
                focusTransitionObserver,
                desktopModeEventLogger,
                desktopTilingDecorViewModel);
    }
@@ -881,6 +880,67 @@ public abstract class WMShellModule {
                        context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor);
    }

    @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(
@@ -1234,7 +1294,8 @@ public abstract class WMShellModule {
            DragAndDropController dragAndDropController,
            @NonNull LetterboxTransitionObserver letterboxTransitionObserver,
            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional,
            Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler) {
            Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler,
            Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler) {
        return new Object();
    }

+97 −0
Original line number Diff line number Diff line
/*
 * 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"
    }
}
+1 −56
Original line number Diff line number Diff line
@@ -35,9 +35,6 @@ import android.graphics.Point
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.os.UserHandle
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.TransitionInfo.Change
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.jank.InteractionJankMonitor
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.ShellCommandHandler
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 @@ class DesktopTasksController(
    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 @@ class DesktopTasksController(
    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 @@ class DesktopTasksController(
            }
        )
        dragAndDropController.addListener(this)
        if (useKeyGestureEventHandler() && enableMoveToNextDisplayShortcut()) {
            inputManager.registerKeyGestureEventHandler(this)
        }
    }

    @VisibleForTesting
@@ -1835,26 +1819,12 @@ class DesktopTasksController(
        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 @@ class DesktopTasksController(
        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 {
+198 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+0 −62

File changed.

Preview size limit exceeded, changes collapsed.