diff options
11 files changed, 705 insertions, 250 deletions
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 24951c4d516e..711dc3a2cf7c 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -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); } 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 4cba8a0ff14a..96f80245c76c 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,9 @@ 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; import android.annotation.Nullable; import android.app.KeyguardManager; @@ -77,6 +80,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; @@ -759,8 +763,6 @@ public abstract class WMShellModule { dragToDesktopTransitionHandler, desktopImmersiveController.get(), desktopRepository, - desktopModeLoggerTransitionObserver, - launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor, @@ -768,8 +770,6 @@ public abstract class WMShellModule { recentTasksController.orElse(null), interactionJankMonitor, mainHandler, - inputManager, - focusTransitionObserver, desktopModeEventLogger, desktopTilingDecorViewModel); } @@ -884,6 +884,72 @@ public abstract class WMShellModule { @WMSingleton @Provides + static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler( + Context context, + Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel, + Optional<DesktopTasksController> desktopTasksController, + InputManager inputManager, + ShellTaskOrganizer shellTaskOrganizer, + FocusTransitionObserver focusTransitionObserver) { + if (DesktopModeStatus.canEnterDesktopMode(context) && useKeyGestureEventHandler() + && manageKeyGestures() + && (Flags.enableMoveToNextDisplayShortcut() + || Flags.enableTaskResizingKeyboardShortcuts())) { + return Optional.of(new DesktopModeKeyGestureHandler(context, + desktopModeWindowDecorViewModel, desktopTasksController, + inputManager, shellTaskOrganizer, focusTransitionObserver)); + } + return Optional.empty(); + } + + @WMSingleton + @Provides + static Optional<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 + ) { + 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, + rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser, + assistContentRequester, multiInstanceHelper, desktopTasksLimiter, + appHandleEducationController, appToWebEducationController, + windowDecorCaptionHandleRepository, activityOrientationChangeHandler, + focusTransitionObserver, desktopModeEventLogger)); + } + + @WMSingleton + @Provides static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( Transitions transitions, Optional<DesktopTasksLimiter> desktopTasksLimiter, @@ -1236,7 +1302,8 @@ public abstract class WMShellModule { @NonNull LetterboxTransitionObserver letterboxTransitionObserver, @NonNull LetterboxCommandHandler letterboxCommandHandler, 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 000000000000..ac07eaa695c1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt @@ -0,0 +1,144 @@ +/* + * 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.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.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 + +/** + * Handles key gesture events (keyboard shortcuts) in Desktop Mode. + */ +class DesktopModeKeyGestureHandler( + private val context: Context, + private val desktopModeWindowDecorViewModel: Optional<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 + || !desktopModeWindowDecorViewModel.isPresent) { + return false + } + when (event.keyGestureType) { + KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> { + logV("Key gesture MOVE_TO_NEXT_DISPLAY is handled") + getGloballyFocusedFreeformTask()?.let { + desktopTasksController.get().moveToNextDisplay( + it.taskId + ) + } + return true + } + // 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 + } + } + + 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 + } + + // 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 4db0be5c9025..8bad87480985 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.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,12 @@ 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.R 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 +110,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 +149,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 +156,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 +236,6 @@ class DesktopTasksController( } ) dragAndDropController.addListener(this) - if (useKeyGestureEventHandler() && enableMoveToNextDisplayShortcut()) { - inputManager.registerKeyGestureEventHandler(this) - } } @VisibleForTesting @@ -793,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) } @@ -871,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) } @@ -961,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, @@ -993,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, @@ -1010,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, @@ -1021,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" ) @@ -1046,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" ) @@ -1064,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() @@ -1835,26 +1857,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. @@ -1996,7 +2004,7 @@ class DesktopTasksController( } } IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { - handleSnapResizingTask( + handleSnapResizingTaskOnDrag( taskInfo, SnapPosition.LEFT, taskSurface, @@ -2007,7 +2015,7 @@ class DesktopTasksController( ) } IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { - handleSnapResizingTask( + handleSnapResizingTaskOnDrag( taskInfo, SnapPosition.RIGHT, taskSurface, @@ -2256,31 +2264,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 { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index d71e61a4c4de..f89b0d108bad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -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( - decoration.mTaskInfo, - decoration.mTaskSurface, - decoration.mTaskInfo.configuration.windowConfiguration.getBounds(), - left ? SnapPosition.LEFT : SnapPosition.RIGHT, - resizeTrigger, - motionEvent, - mWindowDecorByTaskId.get(taskId)); - } + mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler, + Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable"); + mDesktopTasksController.handleInstantSnapResizingTask( + decoration.mTaskInfo, + left ? SnapPosition.LEFT : SnapPosition.RIGHT, + left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU, + motionEvent, + decoration); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 9cb9d2594878..723bbd318803 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -206,7 +206,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopRepository mDesktopRepository; - DesktopModeWindowDecoration( + public DesktopModeWindowDecoration( Context context, @NonNull Context userContext, DisplayController displayController, 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 000000000000..9e63a6d922b8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt @@ -0,0 +1,312 @@ +/* + * 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.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS +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.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger +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, + Optional.of(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) + } + + @Test + @EnableFlags( + FLAG_USE_KEY_GESTURE_EVENT_HANDLER, + FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + ) + fun keyGestureSnapLeft_shouldSnapResizeTaskToLeft() { + desktopModeKeyGestureHandler = DesktopModeKeyGestureHandler( + context, + Optional.of(desktopModeWindowDecorViewModel), Optional.of(desktopTasksController), + inputManager, shellTaskOrganizer, focusTransitionObserver + ) + val task = setUpFreeformTask() + 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_SNAP_LEFT_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET)) + .setModifierState(KeyEvent.META_META_ON) + .build() + val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + + assertThat(result).isTrue() + verify(desktopModeWindowDecorViewModel).onSnapResize(task.taskId, true, null) + } + + @Test + @EnableFlags( + FLAG_USE_KEY_GESTURE_EVENT_HANDLER, + FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + ) + fun keyGestureSnapRight_shouldSnapResizeTaskToRight() { + desktopModeKeyGestureHandler = DesktopModeKeyGestureHandler( + context, + Optional.of(desktopModeWindowDecorViewModel), Optional.of(desktopTasksController), + inputManager, shellTaskOrganizer, focusTransitionObserver + ) + val task = setUpFreeformTask() + 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_SNAP_RIGHT_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET)) + .setModifierState(KeyEvent.META_META_ON) + .build() + val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + + assertThat(result).isTrue() + verify(desktopModeWindowDecorViewModel).onSnapResize(task.taskId, false, null) + } + + @Test + @EnableFlags( + FLAG_USE_KEY_GESTURE_EVENT_HANDLER, + FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + ) + fun keyGestureToggleFreeformWindowSize_shouldToggleTaskSize() { + desktopModeKeyGestureHandler = DesktopModeKeyGestureHandler( + context, + Optional.of(desktopModeWindowDecorViewModel), Optional.of(desktopTasksController), + inputManager, shellTaskOrganizer, focusTransitionObserver + ) + val task = setUpFreeformTask() + 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_TOGGLE_MAXIMIZE_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS)) + .setModifierState(KeyEvent.META_META_ON) + .build() + val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + + assertThat(result).isTrue() + verify(desktopTasksController).toggleDesktopTaskSize( + task, + ResizeTrigger.MAXIMIZE_MENU, + null + ) + } + + @Test + @EnableFlags( + FLAG_USE_KEY_GESTURE_EVENT_HANDLER, + FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + ) + fun keyGestureMinimizeFreeformWindow_shouldMinimizeTask() { + desktopModeKeyGestureHandler = DesktopModeKeyGestureHandler( + context, + Optional.of(desktopModeWindowDecorViewModel), Optional.of(desktopTasksController), + inputManager, shellTaskOrganizer, focusTransitionObserver + ) + val task = setUpFreeformTask() + 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_MINIMIZE_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS)) + .setModifierState(KeyEvent.META_META_ON) + .build() + val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + + assertThat(result).isTrue() + verify(desktopTasksController).minimizeTask(task) + } + + 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 2319716617bf..93999476316a 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 @@ -36,12 +36,10 @@ import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.content.res.Resources 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 +51,6 @@ import android.testing.AndroidTestingRunner 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 @@ -63,6 +60,7 @@ import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT +import android.widget.Toast import android.window.DisplayAreaInfo import android.window.IWindowContainerToken import android.window.RemoteTransition @@ -75,18 +73,14 @@ import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_R 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 +90,6 @@ import com.android.wm.shell.TestRunningTaskInfoBuilder 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 +117,6 @@ import com.android.wm.shell.splitscreen.SplitScreenController 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 +200,10 @@ class DesktopTasksControllerTest : ShellTestCase() { @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,23 +214,21 @@ class DesktopTasksControllerTest : ShellTestCase() { @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 - + @Mock private lateinit var mockToast: Toast private lateinit var mockitoSession: StaticMockitoSession @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration + @Mock private lateinit var resources: Resources private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit private lateinit var taskRepository: DesktopRepository private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener private lateinit var testScope: CoroutineScope - private lateinit var keyGestureEventHandler: KeyGestureEventHandler private val shellExecutor = TestShellExecutor() @@ -263,6 +251,7 @@ class DesktopTasksControllerTest : ShellTestCase() { mockitoSession() .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) + .spyStatic(Toast::class.java) .startMocking() doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } @@ -290,6 +279,7 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( Desktop.getDefaultInstance() ) + doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) } val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN @@ -305,11 +295,6 @@ class DesktopTasksControllerTest : ShellTestCase() { 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 +328,6 @@ class DesktopTasksControllerTest : ShellTestCase() { dragToDesktopTransitionHandler, mMockDesktopImmersiveController, taskRepository, - desktopModeLoggerTransitionObserver, - launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, shellExecutor, @@ -352,8 +335,6 @@ class DesktopTasksControllerTest : ShellTestCase() { recentTasksController, mockInteractionJankMonitor, mockHandler, - mockInputManager, - mockFocusTransitionObserver, desktopModeEventLogger, desktopTilingDecorViewModel, ) @@ -1558,44 +1539,6 @@ class DesktopTasksControllerTest : ShellTestCase() { } @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() @@ -3465,7 +3408,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, Flags.FLAG_ENABLE_TILE_RESIZING) - fun handleSnapResizingTask_nonResizable_snapsToHalfScreen() { + fun handleSnapResizingTaskOnDrag_nonResizable_snapsToHalfScreen() { val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { isResizeable = false } @@ -3474,7 +3417,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val expectedBounds = Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom) - controller.handleSnapResizingTask( + controller.handleSnapResizingTaskOnDrag( task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent, desktopWindowDecoration @@ -3493,14 +3436,14 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) - fun handleSnapResizingTask_nonResizable_startsRepositionAnimation() { + fun handleSnapResizingTaskOnDrag_nonResizable_startsRepositionAnimation() { val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { isResizeable = false } val preDragBounds = Rect(100, 100, 400, 500) val currentDragBounds = Rect(0, 100, 300, 500) - controller.handleSnapResizingTask( + controller.handleSnapResizingTaskOnDrag( task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent, desktopWindowDecoration) verify(mReturnToDragStartAnimator).start( @@ -3520,6 +3463,59 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING + ) + fun handleInstantSnapResizingTask_nonResizable_animatorNotStartedAndShowsToast() { + val taskBounds = Rect(0, 0, 200, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { + isResizeable = false + } + + controller.handleInstantSnapResizingTask( + task, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration) + + // Assert that task is NOT updated via WCT + verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + verify(mockToast).show() + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING) + fun handleInstantSnapResizingTask_resizable_snapsToHalfScreenAndNotShowToast() { + val taskBounds = Rect(0, 0, 200, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { + isResizeable = true + } + val expectedBounds = Rect( + STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom + ) + + controller.handleInstantSnapResizingTask( + task, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration) + + // Assert bounds set to half of the stable bounds + val wct = getLatestToggleResizeDesktopTaskWct(taskBounds) + assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + verify(mockToast, never()).show() + verify(desktopModeEventLogger, times(1)).logTaskResizingStarted( + ResizeTrigger.SNAP_LEFT_MENU, + motionEvent, + task, + displayController + ) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.SNAP_LEFT_MENU, + motionEvent, + task, + expectedBounds.height(), + expectedBounds.width(), + displayController + ) + } + + @Test fun toggleBounds_togglesToCalculatedBoundsForNonResizable() { val bounds = Rect(0, 0, 200, 100) val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { 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 be664f86e9f5..ef9b30c1c4ca 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 @@ -63,7 +63,6 @@ import android.view.SurfaceView import android.view.View import android.view.ViewRootImpl import android.view.WindowInsets.Type.statusBars -import android.widget.Toast import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp import androidx.test.filters.SmallTest @@ -186,7 +185,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser @Mock private lateinit var mockUserHandle: UserHandle @Mock private lateinit var mockAssistContentRequester: AssistContentRequester - @Mock private lateinit var mockToast: Toast private val bgExecutor = TestShellExecutor() @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper @Mock private lateinit var mockTasksLimiter: DesktopTasksLimiter @@ -226,7 +224,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .spyStatic(DragPositioningCallbackUtility::class.java) - .spyStatic(Toast::class.java) .startMocking() doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) } @@ -290,8 +287,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { ) .thenReturn(mockTaskPositioner) - doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) } - // InputChannel cannot be mocked because it passes to InputEventReceiver. val inputChannels = InputChannel.openInputChannelPair(TAG) inputChannels.first().dispose() @@ -640,7 +635,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Test fun testOnDecorSnappedLeft_snapResizes() { - val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -648,19 +642,15 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor ) - val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onLeftSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController).snapToHalfScreen( + verify(mockDesktopTasksController).handleInstantSnapResizingTask( eq(decor.mTaskInfo), - taskSurfaceCaptor.capture(), - eq(currentBounds), eq(SnapPosition.LEFT), eq(ResizeTrigger.SNAP_LEFT_MENU), eq(null), eq(decor) ) - assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface) } @Test @@ -681,7 +671,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) fun testOnSnapResizeLeft_nonResizable_decorSnappedLeft() { - val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -689,19 +678,15 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor ).also { it.mTaskInfo.isResizeable = false } - val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onLeftSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController).snapToHalfScreen( + verify(mockDesktopTasksController).handleInstantSnapResizingTask( eq(decor.mTaskInfo), - taskSurfaceCaptor.capture(), - eq(currentBounds), eq(SnapPosition.LEFT), eq(ResizeTrigger.SNAP_LEFT_MENU), eq(null), eq(decor), ) - assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @Test @@ -723,12 +708,10 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { eq(null), eq(decor), ) - verify(mockToast).show() } @Test fun testOnDecorSnappedRight_snapResizes() { - val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onRightSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -736,19 +719,15 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor ) - val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onRightSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController).snapToHalfScreen( + verify(mockDesktopTasksController).handleInstantSnapResizingTask( eq(decor.mTaskInfo), - taskSurfaceCaptor.capture(), - eq(currentBounds), eq(SnapPosition.RIGHT), eq(ResizeTrigger.SNAP_RIGHT_MENU), eq(null), eq(decor), ) - assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @Test @@ -769,7 +748,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) fun testOnSnapResizeRight_nonResizable_decorSnappedRight() { - val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onRightSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -777,19 +755,15 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor ).also { it.mTaskInfo.isResizeable = false } - val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onRightSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController).snapToHalfScreen( + verify(mockDesktopTasksController).handleInstantSnapResizingTask( eq(decor.mTaskInfo), - taskSurfaceCaptor.capture(), - eq(currentBounds), eq(SnapPosition.RIGHT), eq(ResizeTrigger.SNAP_RIGHT_MENU), eq(null), eq(decor), ) - assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @Test @@ -811,7 +785,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { eq(null), eq(decor), ) - verify(mockToast).show() } @Test diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 6f3540221b63..73d563069632 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -228,6 +228,28 @@ final class InputGestureManager { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)); } + if (enableTaskResizingKeyboardShortcuts()) { + systemShortcuts.add(createKeyGesture( + KeyEvent.KEYCODE_LEFT_BRACKET, + KeyEvent.META_META_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW + )); + systemShortcuts.add(createKeyGesture( + KeyEvent.KEYCODE_RIGHT_BRACKET, + KeyEvent.META_META_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW + )); + systemShortcuts.add(createKeyGesture( + KeyEvent.KEYCODE_EQUALS, + KeyEvent.META_META_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW + )); + systemShortcuts.add(createKeyGesture( + KeyEvent.KEYCODE_MINUS, + KeyEvent.META_META_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW + )); + } if (keyboardA11yShortcutControl()) { if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { systemShortcuts.add(createKeyGesture( @@ -257,28 +279,6 @@ final class InputGestureManager { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS )); } - if (enableTaskResizingKeyboardShortcuts()) { - systemShortcuts.add(createKeyGesture( - KeyEvent.KEYCODE_LEFT_BRACKET, - KeyEvent.META_ALT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW - )); - systemShortcuts.add(createKeyGesture( - KeyEvent.KEYCODE_RIGHT_BRACKET, - KeyEvent.META_ALT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW - )); - systemShortcuts.add(createKeyGesture( - KeyEvent.KEYCODE_EQUALS, - KeyEvent.META_ALT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW - )); - systemShortcuts.add(createKeyGesture( - KeyEvent.KEYCODE_MINUS, - KeyEvent.META_ALT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE - )); - } } synchronized (mGestureLock) { for (InputGestureData systemShortcut : systemShortcuts) { diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index d1f866843be6..34350ab98046 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -676,47 +676,47 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "ALT + [ -> Resizes a task to fit the left half of the screen", + "META + [ -> Resizes a task to fit the left half of the screen", intArrayOf( - KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_LEFT_BRACKET ), KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET), - KeyEvent.META_ALT_ON, + KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "ALT + ] -> Resizes a task to fit the right half of the screen", + "META + ] -> Resizes a task to fit the right half of the screen", intArrayOf( - KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_RIGHT_BRACKET ), KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET), - KeyEvent.META_ALT_ON, + KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "ALT + '=' -> Maximizes a task to fit the screen", + "META + '=' -> Toggles maximization of a task to maximized and restore its bounds", intArrayOf( - KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_EQUALS ), - KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_EQUALS), - KeyEvent.META_ALT_ON, + KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "ALT + '-' -> Restores a task size to its previous bounds", + "META + '-' -> Minimizes a freeform task", intArrayOf( - KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_MINUS ), - KeyGestureEvent.KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE, + KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW, intArrayOf(KeyEvent.KEYCODE_MINUS), - KeyEvent.META_ALT_ON, + KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( |