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

Commit 67c1c7bb authored by Asmita Poddar's avatar Asmita Poddar Committed by Android (Google) Code Review
Browse files

Merge "Add finalised mouse keys bindings and scroll toggle" into main

parents 9426d4b1 4bb378ed
Loading
Loading
Loading
Loading
+0 −4
Original line number Diff line number Diff line
@@ -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;
@@ -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;

/**
@@ -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);
+100 −99
Original line number Diff line number Diff line
@@ -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;
@@ -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'
@@ -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;
@@ -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) {
@@ -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();

    }
@@ -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) {
@@ -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>
     */
@@ -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)
@@ -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);
@@ -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);
@@ -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();
    }

    /**
@@ -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(
@@ -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;
    }
@@ -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) {
@@ -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
@@ -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);
@@ -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.
@@ -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() {
@@ -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) {
    }

}
+34 −11
Original line number Diff line number Diff line
@@ -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
@@ -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.
@@ -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)
@@ -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)

@@ -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)
@@ -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)
@@ -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)
@@ -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>) {