Loading services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +0 −4 Original line number Diff line number Diff line Loading @@ -26,7 +26,6 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.content.Context; import android.graphics.Region; import android.hardware.input.InputManager; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; Loading Loading @@ -56,7 +55,6 @@ import com.android.server.policy.WindowManagerPolicy; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; import java.util.StringJoiner; /** Loading Loading @@ -748,8 +746,6 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if ((mEnabledFeatures & FLAG_FEATURE_MOUSE_KEYS) != 0) { mMouseKeysInterceptor = new MouseKeysInterceptor(mAms, Objects.requireNonNull(mContext.getSystemService( InputManager.class)), Looper.myLooper(), Display.DEFAULT_DISPLAY); addFirstEventHandler(Display.DEFAULT_DISPLAY, mMouseKeysInterceptor); Loading services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java +100 −99 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.hardware.input.InputManager; import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseConfig; Loading Loading @@ -60,8 +59,8 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; * In case multiple physical keyboard are connected to a device, * mouse keys of each physical keyboard will control a single (global) mouse pointer. */ public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback, InputManager.InputDeviceListener { public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback { private static final String LOG_TAG = "MouseKeysInterceptor"; // To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG' Loading @@ -77,11 +76,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen private static final int INTERVAL_MILLIS = 10; private final AccessibilityManagerService mAms; private final InputManager mInputManager; private final Handler mHandler; private final int mDisplayId; VirtualDeviceManager.VirtualDevice mVirtualDevice = null; private VirtualMouse mVirtualMouse = null; Loading @@ -100,23 +96,23 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** Last time the key action was performed */ private long mLastTimeKeyActionPerformed = 0; // TODO (b/346706749): This is currently using the numpad key bindings for mouse keys. // Decide the final mouse key bindings with UX input. /** Whether scroll toggle is on */ private boolean mScrollToggleOn = false; public enum MouseKeyEvent { DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_1), DOWN_MOVE(KeyEvent.KEYCODE_NUMPAD_2), DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_3), LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_4), RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_6), DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_7), UP_MOVE(KeyEvent.KEYCODE_NUMPAD_8), DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_9), LEFT_CLICK(KeyEvent.KEYCODE_NUMPAD_5), RIGHT_CLICK(KeyEvent.KEYCODE_NUMPAD_DOT), HOLD(KeyEvent.KEYCODE_NUMPAD_MULTIPLY), RELEASE(KeyEvent.KEYCODE_NUMPAD_SUBTRACT), SCROLL_UP(KeyEvent.KEYCODE_A), SCROLL_DOWN(KeyEvent.KEYCODE_S); DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7), UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8), DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_9), LEFT_MOVE(KeyEvent.KEYCODE_U), RIGHT_MOVE(KeyEvent.KEYCODE_O), DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_J), DOWN_MOVE_OR_SCROLL(KeyEvent.KEYCODE_K), DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_L), LEFT_CLICK(KeyEvent.KEYCODE_I), RIGHT_CLICK(KeyEvent.KEYCODE_SLASH), HOLD(KeyEvent.KEYCODE_M), RELEASE(KeyEvent.KEYCODE_COMMA), SCROLL_TOGGLE(KeyEvent.KEYCODE_PERIOD); private final int mKeyCode; MouseKeyEvent(int enumValue) { Loading Loading @@ -149,22 +145,19 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * Construct a new MouseKeysInterceptor. * * @param service The service to notify of key events * @param inputManager InputManager to track changes to connected input devices * @param looper Looper to use for callbacks and messages * @param displayId Display ID to send mouse events to */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public MouseKeysInterceptor(AccessibilityManagerService service, InputManager inputManager, Looper looper, int displayId) { public MouseKeysInterceptor(AccessibilityManagerService service, Looper looper, int displayId) { mAms = service; mInputManager = inputManager; mHandler = new Handler(looper, this); mInputManager.registerInputDeviceListener(this, mHandler); mDisplayId = displayId; // Create the virtual mouse on a separate thread since virtual device creation // should happen on an auxiliary thread, and not from the handler's thread. // This is because virtual device creation is a blocking operation and can cause a // deadlock if it is called from the handler's thread. new Thread(() -> { mVirtualMouse = createVirtualMouse(); mVirtualMouse = createVirtualMouse(displayId); }).start(); } Loading Loading @@ -193,22 +186,23 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** * Performs a mouse scroll action based on the provided key code. * The scroll action will only be performed if the scroll toggle is on. * This method interprets the key code as a mouse scroll and sends * the corresponding {@code VirtualMouseScrollEvent#mYAxisMovement}. * @param keyCode The key code representing the mouse scroll action. * Supported keys are: * <ul> * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_UP} * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_DOWN} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL} * </ul> */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void performMouseScrollAction(int keyCode) { MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); float y = switch (mouseKeyEvent) { case SCROLL_UP -> 1.0f; case SCROLL_DOWN -> -1.0f; case UP_MOVE_OR_SCROLL -> 1.0f; case DOWN_MOVE_OR_SCROLL -> -1.0f; default -> 0.0f; }; if (mVirtualMouse != null) { Loading @@ -231,8 +225,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @param keyCode The key code representing the mouse button action. * Supported keys are: * <ul> * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT_CLICK} (Primary Button) * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT_CLICK} (Secondary * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_CLICK} (Primary Button) * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_CLICK} (Secondary * Button) * </ul> */ Loading Loading @@ -264,17 +258,20 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * The method calculates the relative movement of the mouse pointer * and sends the corresponding event to the virtual mouse. * * The UP and DOWN pointer actions will only take place for their respective keys * if the scroll toggle is off. * * @param keyCode The key code representing the direction or button press. * Supported keys are: * <ul> * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_LEFT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DOWN} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_RIGHT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_LEFT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent UP} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_RIGHT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_LEFT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_RIGHT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_LEFT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_RIGHT_MOVE} * </ul> */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) Loading @@ -287,9 +284,11 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); } case DOWN_MOVE -> { case DOWN_MOVE_OR_SCROLL -> { if (!mScrollToggleOn) { y = MOUSE_POINTER_MOVEMENT_STEP; } } case DIAGONAL_DOWN_RIGHT_MOVE -> { x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); Loading @@ -304,9 +303,11 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); } case UP_MOVE -> { case UP_MOVE_OR_SCROLL -> { if (!mScrollToggleOn) { y = -MOUSE_POINTER_MOVEMENT_STEP; } } case DIAGONAL_UP_RIGHT_MOVE -> { x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); Loading @@ -333,8 +334,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } private boolean isMouseScrollKey(int keyCode) { return keyCode == MouseKeyEvent.SCROLL_UP.getKeyCodeValue() || keyCode == MouseKeyEvent.SCROLL_DOWN.getKeyCodeValue(); return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCodeValue() || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCodeValue(); } /** Loading @@ -343,7 +344,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @return The created VirtualMouse. */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private VirtualMouse createVirtualMouse() { private VirtualMouse createVirtualMouse(int displayId) { final VirtualDeviceManagerInternal localVdm = LocalServices.getService(VirtualDeviceManagerInternal.class); mVirtualDevice = localVdm.createVirtualDevice( Loading @@ -351,7 +352,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse( new VirtualMouseConfig.Builder() .setInputDeviceName("Mouse Keys Virtual Mouse") .setAssociatedDisplayId(mDisplayId) .setAssociatedDisplayId(displayId) .build()); return virtualMouse; } Loading @@ -375,15 +376,25 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen if (!isMouseKey(keyCode)) { // Pass non-mouse key events to the next handler super.onKeyEvent(event, policyFlags); } else if (isDown) { if (keyCode == MouseKeyEvent.SCROLL_TOGGLE.getKeyCodeValue()) { mScrollToggleOn = !mScrollToggleOn; if (DEBUG) { Slog.d(LOG_TAG, "Scroll toggle " + (mScrollToggleOn ? "ON" : "OFF")); } } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) { sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); sendVirtualMouseButtonEvent( VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS ); } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) { sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE); } else if (isDown && isMouseButtonKey(keyCode)) { sendVirtualMouseButtonEvent( VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE ); } else if (isMouseButtonKey(keyCode)) { performMouseButtonAction(keyCode); } else if (isDown && isMouseScrollKey(keyCode)) { } else if (mScrollToggleOn && isMouseScrollKey(keyCode)) { // If the scroll key is pressed down and no other key is active, // set it as the active key and send a message to scroll the pointer if (mActiveScrollKey == KEY_NOT_SET) { Loading @@ -391,7 +402,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mLastTimeKeyActionPerformed = event.getDownTime(); mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER); } } else if (isDown) { } else { // This is a directional key. // If the key is pressed down and no other key is active, // set it as the active key and send a message to move the pointer Loading @@ -400,7 +411,10 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mLastTimeKeyActionPerformed = event.getDownTime(); mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER); } } else if (mActiveMoveKey == keyCode) { } } else { // Up event received if (mActiveMoveKey == keyCode) { // If the key is released, and it is the active key, stop moving the pointer mActiveMoveKey = KEY_NOT_SET; mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER); Loading @@ -413,6 +427,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen + "', with no matching down event from deviceId = " + event.getDeviceId()); } } } /** * Handle messages for moving or scrolling the mouse pointer. Loading Loading @@ -470,14 +485,6 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } } @Override public void onInputDeviceAdded(int deviceId) { } @Override public void onInputDeviceRemoved(int deviceId) { } @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @Override public void onDestroy() { Loading @@ -485,14 +492,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mActiveMoveKey = KEY_NOT_SET; mActiveScrollKey = KEY_NOT_SET; mLastTimeKeyActionPerformed = 0; mHandler.removeCallbacksAndMessages(null); mHandler.removeCallbacksAndMessages(null); mVirtualDevice.close(); mInputManager.unregisterInputDeviceListener(this); } @Override public void onInputDeviceChanged(int deviceId) { } } services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt +34 −11 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.accessibility import android.util.MathUtils.sqrt import android.companion.virtual.VirtualDeviceManager import android.companion.virtual.VirtualDeviceParams import android.content.Context Loading Loading @@ -59,6 +61,7 @@ class MouseKeysInterceptorTest { companion object { const val DISPLAY_ID = 1 const val DEVICE_ID = 123 const val MOUSE_POINTER_MOVEMENT_STEP = 1.8f // This delay is required for key events to be sent and handled correctly. // The handler only performs a move/scroll event if it receives the key event // at INTERVAL_MILLIS (which happens in practice). Hence, we need this delay in the tests. Loading Loading @@ -113,8 +116,7 @@ class MouseKeysInterceptorTest { Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager) mouseKeysInterceptor = MouseKeysInterceptor(mockAms, mockInputManager, testLooper.looper, DISPLAY_ID) mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID) // VirtualMouse is created on a separate thread. // Wait for VirtualMouse to be created before running tests TimeUnit.MILLISECONDS.sleep(20L) Loading Loading @@ -145,7 +147,7 @@ class MouseKeysInterceptorTest { fun whenMouseDirectionalKeyIsPressed_relativeEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.DOWN_MOVE.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.DIAGONAL_DOWN_LEFT_MOVE.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) Loading @@ -153,14 +155,15 @@ class MouseKeysInterceptorTest { testLooper.dispatchAll() // Verify the sendRelativeEvent method is called once and capture the arguments verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(1.8f)) verifyRelativeEvents(arrayOf(-MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f)), arrayOf(MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f))) } @Test fun whenClickKeyIsPressed_buttonEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) Loading @@ -179,7 +182,7 @@ class MouseKeysInterceptorTest { @Test fun whenHoldKeyIsPressed_buttonEventIsSent() { val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) Loading @@ -195,7 +198,7 @@ class MouseKeysInterceptorTest { @Test fun whenReleaseKeyIsPressed_buttonEventIsSent() { val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) Loading @@ -209,18 +212,38 @@ class MouseKeysInterceptorTest { } @Test fun whenScrollUpKeyIsPressed_scrollEventIsSent() { fun whenScrollToggleOn_ScrollUpKeyIsPressed_scrollEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.SCROLL_UP.getKeyCodeValue() val keyCodeScrollToggle = MouseKeysInterceptor.MouseKeyEvent.SCROLL_TOGGLE.keyCodeValue val keyCodeScroll = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue val scrollToggleDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCodeScrollToggle, 0, 0, DEVICE_ID, 0) val scrollDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCodeScroll, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(scrollToggleDownEvent, 0) mouseKeysInterceptor.onKeyEvent(scrollDownEvent, 0) testLooper.dispatchAll() // Verify the sendScrollEvent method is called once and capture the arguments verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f)) } @Test fun whenScrollToggleOff_DirectionalUpKeyIsPressed_RelativeEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) testLooper.dispatchAll() // Verify the sendScrollEvent method is called once and capture the arguments verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f)) // Verify the sendRelativeEvent method is called once and capture the arguments verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(-MOUSE_POINTER_MOVEMENT_STEP)) } private fun verifyRelativeEvents(expectedX: Array<Float>, expectedY: Array<Float>) { Loading Loading
services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +0 −4 Original line number Diff line number Diff line Loading @@ -26,7 +26,6 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.content.Context; import android.graphics.Region; import android.hardware.input.InputManager; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; Loading Loading @@ -56,7 +55,6 @@ import com.android.server.policy.WindowManagerPolicy; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; import java.util.StringJoiner; /** Loading Loading @@ -748,8 +746,6 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if ((mEnabledFeatures & FLAG_FEATURE_MOUSE_KEYS) != 0) { mMouseKeysInterceptor = new MouseKeysInterceptor(mAms, Objects.requireNonNull(mContext.getSystemService( InputManager.class)), Looper.myLooper(), Display.DEFAULT_DISPLAY); addFirstEventHandler(Display.DEFAULT_DISPLAY, mMouseKeysInterceptor); Loading
services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java +100 −99 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.hardware.input.InputManager; import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseConfig; Loading Loading @@ -60,8 +59,8 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; * In case multiple physical keyboard are connected to a device, * mouse keys of each physical keyboard will control a single (global) mouse pointer. */ public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback, InputManager.InputDeviceListener { public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback { private static final String LOG_TAG = "MouseKeysInterceptor"; // To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG' Loading @@ -77,11 +76,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen private static final int INTERVAL_MILLIS = 10; private final AccessibilityManagerService mAms; private final InputManager mInputManager; private final Handler mHandler; private final int mDisplayId; VirtualDeviceManager.VirtualDevice mVirtualDevice = null; private VirtualMouse mVirtualMouse = null; Loading @@ -100,23 +96,23 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** Last time the key action was performed */ private long mLastTimeKeyActionPerformed = 0; // TODO (b/346706749): This is currently using the numpad key bindings for mouse keys. // Decide the final mouse key bindings with UX input. /** Whether scroll toggle is on */ private boolean mScrollToggleOn = false; public enum MouseKeyEvent { DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_1), DOWN_MOVE(KeyEvent.KEYCODE_NUMPAD_2), DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_3), LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_4), RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_6), DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_7), UP_MOVE(KeyEvent.KEYCODE_NUMPAD_8), DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_9), LEFT_CLICK(KeyEvent.KEYCODE_NUMPAD_5), RIGHT_CLICK(KeyEvent.KEYCODE_NUMPAD_DOT), HOLD(KeyEvent.KEYCODE_NUMPAD_MULTIPLY), RELEASE(KeyEvent.KEYCODE_NUMPAD_SUBTRACT), SCROLL_UP(KeyEvent.KEYCODE_A), SCROLL_DOWN(KeyEvent.KEYCODE_S); DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7), UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8), DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_9), LEFT_MOVE(KeyEvent.KEYCODE_U), RIGHT_MOVE(KeyEvent.KEYCODE_O), DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_J), DOWN_MOVE_OR_SCROLL(KeyEvent.KEYCODE_K), DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_L), LEFT_CLICK(KeyEvent.KEYCODE_I), RIGHT_CLICK(KeyEvent.KEYCODE_SLASH), HOLD(KeyEvent.KEYCODE_M), RELEASE(KeyEvent.KEYCODE_COMMA), SCROLL_TOGGLE(KeyEvent.KEYCODE_PERIOD); private final int mKeyCode; MouseKeyEvent(int enumValue) { Loading Loading @@ -149,22 +145,19 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * Construct a new MouseKeysInterceptor. * * @param service The service to notify of key events * @param inputManager InputManager to track changes to connected input devices * @param looper Looper to use for callbacks and messages * @param displayId Display ID to send mouse events to */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public MouseKeysInterceptor(AccessibilityManagerService service, InputManager inputManager, Looper looper, int displayId) { public MouseKeysInterceptor(AccessibilityManagerService service, Looper looper, int displayId) { mAms = service; mInputManager = inputManager; mHandler = new Handler(looper, this); mInputManager.registerInputDeviceListener(this, mHandler); mDisplayId = displayId; // Create the virtual mouse on a separate thread since virtual device creation // should happen on an auxiliary thread, and not from the handler's thread. // This is because virtual device creation is a blocking operation and can cause a // deadlock if it is called from the handler's thread. new Thread(() -> { mVirtualMouse = createVirtualMouse(); mVirtualMouse = createVirtualMouse(displayId); }).start(); } Loading Loading @@ -193,22 +186,23 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** * Performs a mouse scroll action based on the provided key code. * The scroll action will only be performed if the scroll toggle is on. * This method interprets the key code as a mouse scroll and sends * the corresponding {@code VirtualMouseScrollEvent#mYAxisMovement}. * @param keyCode The key code representing the mouse scroll action. * Supported keys are: * <ul> * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_UP} * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_DOWN} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL} * </ul> */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void performMouseScrollAction(int keyCode) { MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode); float y = switch (mouseKeyEvent) { case SCROLL_UP -> 1.0f; case SCROLL_DOWN -> -1.0f; case UP_MOVE_OR_SCROLL -> 1.0f; case DOWN_MOVE_OR_SCROLL -> -1.0f; default -> 0.0f; }; if (mVirtualMouse != null) { Loading @@ -231,8 +225,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @param keyCode The key code representing the mouse button action. * Supported keys are: * <ul> * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT_CLICK} (Primary Button) * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT_CLICK} (Secondary * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_CLICK} (Primary Button) * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_CLICK} (Secondary * Button) * </ul> */ Loading Loading @@ -264,17 +258,20 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * The method calculates the relative movement of the mouse pointer * and sends the corresponding event to the virtual mouse. * * The UP and DOWN pointer actions will only take place for their respective keys * if the scroll toggle is off. * * @param keyCode The key code representing the direction or button press. * Supported keys are: * <ul> * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_LEFT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DOWN} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_RIGHT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_LEFT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent UP} * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_RIGHT} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_LEFT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_RIGHT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_LEFT_MOVE} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL} * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_RIGHT_MOVE} * </ul> */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) Loading @@ -287,9 +284,11 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); } case DOWN_MOVE -> { case DOWN_MOVE_OR_SCROLL -> { if (!mScrollToggleOn) { y = MOUSE_POINTER_MOVEMENT_STEP; } } case DIAGONAL_DOWN_RIGHT_MOVE -> { x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); Loading @@ -304,9 +303,11 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); } case UP_MOVE -> { case UP_MOVE_OR_SCROLL -> { if (!mScrollToggleOn) { y = -MOUSE_POINTER_MOVEMENT_STEP; } } case DIAGONAL_UP_RIGHT_MOVE -> { x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2); Loading @@ -333,8 +334,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } private boolean isMouseScrollKey(int keyCode) { return keyCode == MouseKeyEvent.SCROLL_UP.getKeyCodeValue() || keyCode == MouseKeyEvent.SCROLL_DOWN.getKeyCodeValue(); return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCodeValue() || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCodeValue(); } /** Loading @@ -343,7 +344,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @return The created VirtualMouse. */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private VirtualMouse createVirtualMouse() { private VirtualMouse createVirtualMouse(int displayId) { final VirtualDeviceManagerInternal localVdm = LocalServices.getService(VirtualDeviceManagerInternal.class); mVirtualDevice = localVdm.createVirtualDevice( Loading @@ -351,7 +352,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse( new VirtualMouseConfig.Builder() .setInputDeviceName("Mouse Keys Virtual Mouse") .setAssociatedDisplayId(mDisplayId) .setAssociatedDisplayId(displayId) .build()); return virtualMouse; } Loading @@ -375,15 +376,25 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen if (!isMouseKey(keyCode)) { // Pass non-mouse key events to the next handler super.onKeyEvent(event, policyFlags); } else if (isDown) { if (keyCode == MouseKeyEvent.SCROLL_TOGGLE.getKeyCodeValue()) { mScrollToggleOn = !mScrollToggleOn; if (DEBUG) { Slog.d(LOG_TAG, "Scroll toggle " + (mScrollToggleOn ? "ON" : "OFF")); } } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) { sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); sendVirtualMouseButtonEvent( VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS ); } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) { sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE); } else if (isDown && isMouseButtonKey(keyCode)) { sendVirtualMouseButtonEvent( VirtualMouseButtonEvent.BUTTON_PRIMARY, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE ); } else if (isMouseButtonKey(keyCode)) { performMouseButtonAction(keyCode); } else if (isDown && isMouseScrollKey(keyCode)) { } else if (mScrollToggleOn && isMouseScrollKey(keyCode)) { // If the scroll key is pressed down and no other key is active, // set it as the active key and send a message to scroll the pointer if (mActiveScrollKey == KEY_NOT_SET) { Loading @@ -391,7 +402,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mLastTimeKeyActionPerformed = event.getDownTime(); mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER); } } else if (isDown) { } else { // This is a directional key. // If the key is pressed down and no other key is active, // set it as the active key and send a message to move the pointer Loading @@ -400,7 +411,10 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mLastTimeKeyActionPerformed = event.getDownTime(); mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER); } } else if (mActiveMoveKey == keyCode) { } } else { // Up event received if (mActiveMoveKey == keyCode) { // If the key is released, and it is the active key, stop moving the pointer mActiveMoveKey = KEY_NOT_SET; mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER); Loading @@ -413,6 +427,7 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen + "', with no matching down event from deviceId = " + event.getDeviceId()); } } } /** * Handle messages for moving or scrolling the mouse pointer. Loading Loading @@ -470,14 +485,6 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } } @Override public void onInputDeviceAdded(int deviceId) { } @Override public void onInputDeviceRemoved(int deviceId) { } @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @Override public void onDestroy() { Loading @@ -485,14 +492,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mActiveMoveKey = KEY_NOT_SET; mActiveScrollKey = KEY_NOT_SET; mLastTimeKeyActionPerformed = 0; mHandler.removeCallbacksAndMessages(null); mHandler.removeCallbacksAndMessages(null); mVirtualDevice.close(); mInputManager.unregisterInputDeviceListener(this); } @Override public void onInputDeviceChanged(int deviceId) { } }
services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt +34 −11 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.accessibility import android.util.MathUtils.sqrt import android.companion.virtual.VirtualDeviceManager import android.companion.virtual.VirtualDeviceParams import android.content.Context Loading Loading @@ -59,6 +61,7 @@ class MouseKeysInterceptorTest { companion object { const val DISPLAY_ID = 1 const val DEVICE_ID = 123 const val MOUSE_POINTER_MOVEMENT_STEP = 1.8f // This delay is required for key events to be sent and handled correctly. // The handler only performs a move/scroll event if it receives the key event // at INTERVAL_MILLIS (which happens in practice). Hence, we need this delay in the tests. Loading Loading @@ -113,8 +116,7 @@ class MouseKeysInterceptorTest { Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager) mouseKeysInterceptor = MouseKeysInterceptor(mockAms, mockInputManager, testLooper.looper, DISPLAY_ID) mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID) // VirtualMouse is created on a separate thread. // Wait for VirtualMouse to be created before running tests TimeUnit.MILLISECONDS.sleep(20L) Loading Loading @@ -145,7 +147,7 @@ class MouseKeysInterceptorTest { fun whenMouseDirectionalKeyIsPressed_relativeEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.DOWN_MOVE.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.DIAGONAL_DOWN_LEFT_MOVE.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) Loading @@ -153,14 +155,15 @@ class MouseKeysInterceptorTest { testLooper.dispatchAll() // Verify the sendRelativeEvent method is called once and capture the arguments verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(1.8f)) verifyRelativeEvents(arrayOf(-MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f)), arrayOf(MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f))) } @Test fun whenClickKeyIsPressed_buttonEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) Loading @@ -179,7 +182,7 @@ class MouseKeysInterceptorTest { @Test fun whenHoldKeyIsPressed_buttonEventIsSent() { val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) Loading @@ -195,7 +198,7 @@ class MouseKeysInterceptorTest { @Test fun whenReleaseKeyIsPressed_buttonEventIsSent() { val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.getKeyCodeValue() val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) Loading @@ -209,18 +212,38 @@ class MouseKeysInterceptorTest { } @Test fun whenScrollUpKeyIsPressed_scrollEventIsSent() { fun whenScrollToggleOn_ScrollUpKeyIsPressed_scrollEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.SCROLL_UP.getKeyCodeValue() val keyCodeScrollToggle = MouseKeysInterceptor.MouseKeyEvent.SCROLL_TOGGLE.keyCodeValue val keyCodeScroll = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue val scrollToggleDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCodeScrollToggle, 0, 0, DEVICE_ID, 0) val scrollDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCodeScroll, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(scrollToggleDownEvent, 0) mouseKeysInterceptor.onKeyEvent(scrollDownEvent, 0) testLooper.dispatchAll() // Verify the sendScrollEvent method is called once and capture the arguments verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f)) } @Test fun whenScrollToggleOff_DirectionalUpKeyIsPressed_RelativeEventIsSent() { // There should be some delay between the downTime of the key event and calling onKeyEvent val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS val keyCode = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, DEVICE_ID, 0) mouseKeysInterceptor.onKeyEvent(downEvent, 0) testLooper.dispatchAll() // Verify the sendScrollEvent method is called once and capture the arguments verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f)) // Verify the sendRelativeEvent method is called once and capture the arguments verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(-MOUSE_POINTER_MOVEMENT_STEP)) } private fun verifyRelativeEvents(expectedX: Array<Float>, expectedY: Array<Float>) { Loading