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

Commit 787ab1cf authored by Vania Desmonda's avatar Vania Desmonda
Browse files

Add keyboard shortcuts implementation for Desktop Windowing task

resizing keyboard shortcuts (snap left/right, toggle window size and
minimize task)

Flag: com.android.window.flags.enable_task_resizing_keyboard_shortcuts
Test: atest DesktopModeKeyGestureHandlerTest
Bug: 335819608
Change-Id: I4c5318ccfda0043e13b377196cb35c78a7815da8
parent 82ffdfea
Loading
Loading
Loading
Loading
+13 −16
Original line number Diff line number Diff line
@@ -115,12 +115,14 @@ public final class KeyGestureEvent {
    public static final int KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS = 67;
    public static final int KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW = 68;
    public static final int KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW = 69;
    public static final int KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW = 70;
    public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71;
    public static final int KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW = 70;
    public static final int KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW = 71;
    public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN = 72;
    public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT = 73;
    public static final int KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION = 74;
    public static final int KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK = 75;
    public static final int KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW = 76;


    public static final int FLAG_CANCELLED = 1;

@@ -205,12 +207,13 @@ public final class KeyGestureEvent {
            KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS,
            KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
            KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
            KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
            KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE,
            KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW,
            KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
            KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN,
            KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT,
            KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
            KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK,
            KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface KeyGestureType {
@@ -557,14 +560,6 @@ public final class KeyGestureEvent {
                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE;
            case KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION;
            case KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW:
                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SNAP_LEFT_FREEFORM_WINDOW;
            case KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW:
                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SNAP_RIGHT_FREEFORM_WINDOW;
            case KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW:
                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MAXIMIZE_FREEFORM_WINDOW;
            case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE:
                return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RESTORE_FREEFORM_WINDOW_SIZE;
            default:
                return LOG_EVENT_UNSPECIFIED;
        }
@@ -777,10 +772,10 @@ public final class KeyGestureEvent {
                return "KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW";
            case KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW:
                return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW";
            case KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW:
                return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW";
            case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE:
                return "KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE";
            case KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW:
                return "KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW";
            case KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW:
                return "KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW";
            case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN:
                return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN";
            case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT:
@@ -789,6 +784,8 @@ public final class KeyGestureEvent {
                return "KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION";
            case KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK:
                return "KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK";
            case KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW:
                return "KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW";
            default:
                return Integer.toHexString(value);
        }
+11 −5
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ 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.manageKeyGestures;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;

import android.annotation.NonNull;
@@ -884,13 +885,15 @@ public abstract class WMShellModule {
    @Provides
    static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
            Context context,
            DesktopModeWindowDecorViewModel desktopModeWindowDecorViewModel,
            Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel,
            Optional<DesktopTasksController> desktopTasksController,
            InputManager inputManager,
            ShellTaskOrganizer shellTaskOrganizer,
            FocusTransitionObserver focusTransitionObserver) {
        if (DesktopModeStatus.canEnterDesktopMode(context) && useKeyGestureEventHandler()
                && Flags.enableMoveToNextDisplayShortcut()) {
                && manageKeyGestures()
                && (Flags.enableMoveToNextDisplayShortcut()
                || Flags.enableTaskResizingKeyboardShortcuts())) {
            return Optional.of(new DesktopModeKeyGestureHandler(context,
                    desktopModeWindowDecorViewModel, desktopTasksController,
                    inputManager, shellTaskOrganizer, focusTransitionObserver));
@@ -900,7 +903,7 @@ public abstract class WMShellModule {

    @WMSingleton
    @Provides
    static DesktopModeWindowDecorViewModel provideDesktopModeWindowDecorViewModel(
    static Optional<DesktopModeWindowDecorViewModel> provideDesktopModeWindowDecorViewModel(
            Context context,
            @ShellMainThread ShellExecutor shellExecutor,
            @ShellMainThread Handler mainHandler,
@@ -930,7 +933,10 @@ public abstract class WMShellModule {
            FocusTransitionObserver focusTransitionObserver,
            DesktopModeEventLogger desktopModeEventLogger
    ) {
        return new DesktopModeWindowDecorViewModel(context, shellExecutor, mainHandler,
        if (!DesktopModeStatus.canEnterDesktopMode(context)) {
            return Optional.empty();
        }
        return Optional.of(new DesktopModeWindowDecorViewModel(context, shellExecutor, mainHandler,
                mainChoreographer, bgExecutor, shellInit, shellCommandHandler, windowManager,
                taskOrganizer, desktopRepository, displayController, shellController,
                displayInsetsController, syncQueue, transitions, desktopTasksController,
@@ -938,7 +944,7 @@ public abstract class WMShellModule {
                assistContentRequester, multiInstanceHelper, desktopTasksLimiter,
                appHandleEducationController, appToWebEducationController,
                windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
                focusTransitionObserver, desktopModeEventLogger);
                focusTransitionObserver, desktopModeEventLogger));
    }

    @WMSingleton
+61 −14
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
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
@@ -29,6 +28,9 @@ 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.hardware.input.Flags.manageKeyGestures
import com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import java.util.Optional
@@ -38,7 +40,7 @@ import java.util.Optional
 */
class DesktopModeKeyGestureHandler(
    private val context: Context,
    private val desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel,
    private val desktopModeWindowDecorViewModel: Optional<DesktopModeWindowDecorViewModel>,
    private val desktopTasksController: Optional<DesktopTasksController>,
    inputManager: InputManager,
    private val shellTaskOrganizer: ShellTaskOrganizer,
@@ -50,14 +52,12 @@ class DesktopModeKeyGestureHandler(
    }

    override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
        if (!isKeyGestureSupported(event.keyGestureType) || !desktopTasksController.isPresent) {
        if (!isKeyGestureSupported(event.keyGestureType) || !desktopTasksController.isPresent
            || !desktopModeWindowDecorViewModel.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(
@@ -66,7 +66,49 @@ class DesktopModeKeyGestureHandler(
                }
                return true
            }
                return false
            // TODO(b/375356876): Modify function to pass in keyboard shortcut as the input
            // method for logging task resize
            KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW -> {
                logV("Key gesture SNAP_LEFT_FREEFORM_WINDOW is handled")
                getGloballyFocusedFreeformTask()?.let {
                    desktopModeWindowDecorViewModel.get().onSnapResize(
                        it.taskId,
                        true,
                        null
                    )
                }
                return true
            }
            KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW -> {
                logV("Key gesture SNAP_RIGHT_FREEFORM_WINDOW is handled")
                getGloballyFocusedFreeformTask()?.let {
                    desktopModeWindowDecorViewModel.get().onSnapResize(
                        it.taskId,
                        false,
                        null
                    )
                }
                return true
            }
            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW -> {
                logV("Key gesture TOGGLE_MAXIMIZE_FREEFORM_WINDOW is handled")
                getGloballyFocusedFreeformTask()?.let {
                    desktopTasksController.get().toggleDesktopTaskSize(
                        it,
                        ResizeTrigger.MAXIMIZE_MENU,
                        null,
                    )
                }
                return true
            }
            KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW -> {
                logV("Key gesture MINIMIZE_FREEFORM_WINDOW is handled")
                getGloballyFocusedFreeformTask()?.let {
                    desktopTasksController.get().minimizeTask(
                        it,
                    )
                }
                return true
            }
            else -> return false
        }
@@ -75,6 +117,11 @@ class DesktopModeKeyGestureHandler(
    override fun isKeyGestureSupported(gestureType: Int): Boolean = when (gestureType) {
        KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY
            -> enableMoveToNextDisplayShortcut()
        KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
        KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
        KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW
            -> enableTaskResizingKeyboardShortcuts() && manageKeyGestures()
        else -> false
    }

+50 −12
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ 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.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
@@ -777,6 +778,10 @@ class DesktopTasksController(
        resizeTrigger: ResizeTrigger,
        motionEvent: MotionEvent?,
    ) {
        desktopModeEventLogger.logTaskResizingStarted(
            resizeTrigger, motionEvent, taskInfo, displayController
        )

        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return

        val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
@@ -855,9 +860,6 @@ class DesktopTasksController(
            return
        }

        desktopModeEventLogger.logTaskResizingStarted(
            ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent, taskInfo, displayController
        )
        toggleDesktopTaskSize(taskInfo, ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent)
    }

@@ -945,13 +947,17 @@ class DesktopTasksController(
     */
    fun snapToHalfScreen(
        taskInfo: RunningTaskInfo,
        taskSurface: SurfaceControl,
        taskSurface: SurfaceControl?,
        currentDragBounds: Rect,
        position: SnapPosition,
        resizeTrigger: ResizeTrigger,
        motionEvent: MotionEvent?,
        desktopWindowDecoration: DesktopModeWindowDecoration,
    ) {
        desktopModeEventLogger.logTaskResizingStarted(
            resizeTrigger, motionEvent, taskInfo, displayController
        )

        if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) {
            val isTiled = desktopTilingDecorViewModel.snapToHalfScreen(
                taskInfo,
@@ -977,7 +983,7 @@ class DesktopTasksController(
            // Handle the case where we attempt to snap resize when already snap resized: the task
            // position won't need to change but we want to animate the surface going back to the
            // snapped position from the "dragged-to-the-edge" position.
            if (destinationBounds != currentDragBounds) {
            if (destinationBounds != currentDragBounds && taskSurface != null) {
                returnToDragStartAnimator.start(
                    taskInfo.taskId,
                    taskSurface,
@@ -994,8 +1000,40 @@ class DesktopTasksController(
        toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds)
    }

    /**
     * Handles snap resizing a [taskInfo] to [position] instantaneously, for example when the
     * [resizeTrigger] is the snap resize menu using any [motionEvent] or a keyboard shortcut.
     */
    fun handleInstantSnapResizingTask(
        taskInfo: RunningTaskInfo,
        position: SnapPosition,
        resizeTrigger: ResizeTrigger,
        motionEvent: MotionEvent? = null,
        desktopModeWindowDecoration: DesktopModeWindowDecoration,
    ) {
        if (!isSnapResizingAllowed(taskInfo)) {
            Toast.makeText(
                getContext(),
                R.string.desktop_mode_non_resizable_snap_text,
                Toast.LENGTH_SHORT
            ).show()
            return
        }

        snapToHalfScreen(
            taskInfo,
            null,
            taskInfo.configuration.windowConfiguration.bounds,
            position,
            resizeTrigger,
            motionEvent,
            desktopModeWindowDecoration
        )
    }


    @VisibleForTesting
    fun handleSnapResizingTask(
    fun handleSnapResizingTaskOnDrag(
        taskInfo: RunningTaskInfo,
        position: SnapPosition,
        taskSurface: SurfaceControl,
@@ -1005,7 +1043,7 @@ class DesktopTasksController(
        desktopModeWindowDecoration: DesktopModeWindowDecoration,
    ) {
        releaseVisualIndicator()
        if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) {
        if (!isSnapResizingAllowed(taskInfo)) {
            interactionJankMonitor.begin(
                taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_non_resizable"
            )
@@ -1030,9 +1068,6 @@ class DesktopTasksController(
            } else {
                ResizeTrigger.DRAG_RIGHT
            }
            desktopModeEventLogger.logTaskResizingStarted(
                resizeTrigger, motionEvent, taskInfo, displayController
            )
            interactionJankMonitor.begin(
                taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable"
            )
@@ -1048,6 +1083,9 @@ class DesktopTasksController(
        }
    }

    private fun isSnapResizingAllowed(taskInfo: RunningTaskInfo) =
        taskInfo.isResizeable || !DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()

    private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect {
        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect()

@@ -1966,7 +2004,7 @@ class DesktopTasksController(
                }
            }
            IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
                handleSnapResizingTask(
                handleSnapResizingTaskOnDrag(
                    taskInfo,
                    SnapPosition.LEFT,
                    taskSurface,
@@ -1977,7 +2015,7 @@ class DesktopTasksController(
                )
            }
            IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
                handleSnapResizingTask(
                handleSnapResizingTaskOnDrag(
                    taskInfo,
                    SnapPosition.RIGHT,
                    taskSurface,
+9 −26
Original line number Diff line number Diff line
@@ -78,7 +78,6 @@ import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Toast;
import android.window.DesktopModeFlags;
import android.window.TaskSnapshot;
import android.window.WindowContainerToken;
@@ -572,9 +571,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
        if (decoration == null) {
            return;
        }
        mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent,
                decoration.mTaskInfo,
                mDisplayController, /* displayLayoutSize= */ null);
        mInteractionJankMonitor.begin(
                decoration.mTaskSurface, mContext, mMainHandler,
                Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source);
@@ -593,33 +589,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
        decoration.closeMaximizeMenu();
    }

    private void onSnapResize(int taskId, boolean left, MotionEvent motionEvent) {
    public void onSnapResize(int taskId, boolean left, @Nullable MotionEvent motionEvent) {
        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
        if (decoration == null) {
            return;
        }

        if (!decoration.mTaskInfo.isResizeable
                && DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) {
            Toast.makeText(mContext,
                    R.string.desktop_mode_non_resizable_snap_text, Toast.LENGTH_SHORT).show();
        } else {
            ResizeTrigger resizeTrigger =
                    left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU;
            mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent,
                    decoration.mTaskInfo,
                    mDisplayController, /* displayLayoutSize= */ null);
        mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler,
                Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable");
            mDesktopTasksController.snapToHalfScreen(
        mDesktopTasksController.handleInstantSnapResizingTask(
                decoration.mTaskInfo,
                    decoration.mTaskSurface,
                    decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
                left ? SnapPosition.LEFT : SnapPosition.RIGHT,
                    resizeTrigger,
                left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU,
                motionEvent,
                    mWindowDecorByTaskId.get(taskId));
        }
                decoration);

        decoration.closeHandleMenu();
        decoration.closeMaximizeMenu();
Loading