• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.accessibility;
18 
19 import static android.accessibilityservice.AccessibilityTrace.FLAGS_INPUT_FILTER;
20 import static android.util.MathUtils.sqrt;
21 
22 import android.annotation.Nullable;
23 import android.annotation.RequiresPermission;
24 import android.companion.virtual.VirtualDeviceManager;
25 import android.companion.virtual.VirtualDeviceParams;
26 import android.hardware.input.InputManager;
27 import android.hardware.input.VirtualMouse;
28 import android.hardware.input.VirtualMouseButtonEvent;
29 import android.hardware.input.VirtualMouseConfig;
30 import android.hardware.input.VirtualMouseRelativeEvent;
31 import android.hardware.input.VirtualMouseScrollEvent;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.util.Log;
36 import android.util.Slog;
37 import android.util.SparseArray;
38 import android.view.InputDevice;
39 import android.view.KeyEvent;
40 
41 import androidx.annotation.VisibleForTesting;
42 
43 import com.android.server.LocalServices;
44 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
45 
46 /**
47  * Implements the "mouse keys" accessibility feature for physical keyboards.
48  *
49  * If enabled, mouse keys will allow users to use a physical keyboard to
50  * control the mouse on the display.
51  * The following mouse functionality is supported by the mouse keys:
52  * <ul>
53  *   <li> Move the mouse pointer in different directions (up, down, left, right and diagonally).
54  *   <li> Click the mouse button (left, right and middle click).
55  *   <li> Press and hold the mouse button.
56  *   <li> Release the mouse button.
57  *   <li> Scroll (up and down).
58  * </ul>
59  *
60  * The keys that are mapped to mouse keys are consumed by {@link AccessibilityInputFilter}.
61  * Non-mouse key {@link KeyEvent} will be passed to the parent handler to be handled as usual.
62  * A new {@link VirtualMouse} is created whenever the mouse keys feature is turned on in Settings.
63  * In case multiple physical keyboard are connected to a device,
64  * mouse keys of each physical keyboard will control a single (global) mouse pointer.
65  */
66 public class MouseKeysInterceptor extends BaseEventStreamTransformation
67         implements Handler.Callback, InputManager.InputDeviceListener {
68     private static final String LOG_TAG = "MouseKeysInterceptor";
69 
70     // To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG'
71     // (requires restart)
72     private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
73 
74     private static final int MESSAGE_MOVE_MOUSE_POINTER = 1;
75     private static final int MESSAGE_SCROLL_MOUSE_POINTER = 2;
76     private static final int KEY_NOT_SET = -1;
77 
78     /** Time interval after which mouse action will be repeated */
79     private static final int INTERVAL_MILLIS = 10;
80 
81     @VisibleForTesting
82     public static final float MOUSE_POINTER_MOVEMENT_STEP = 1.8f;
83     @VisibleForTesting
84     public static final float MOUSE_SCROLL_STEP = 0.2f;
85 
86     private final AccessibilityManagerService mAms;
87     private final Handler mHandler;
88     private final InputManager mInputManager;
89 
90     /** Thread to wait for virtual mouse creation to complete */
91     private final Thread mCreateVirtualMouseThread;
92 
93     /**
94      * Map of device IDs to a map of key codes to their corresponding {@link MouseKeyEvent} values.
95      * To ensure thread safety for the map, all access and modification of the map
96      * should happen on the same thread, i.e., on the handler thread.
97      */
98     private final SparseArray<SparseArray<MouseKeyEvent>> mDeviceKeyCodeMap =
99             new SparseArray<>();
100 
101     VirtualDeviceManager.VirtualDevice mVirtualDevice = null;
102 
103     private VirtualMouse mVirtualMouse = null;
104 
105     /**
106      * State of the active directional mouse key.
107      * Multiple mouse keys will not be allowed to be used simultaneously i.e.,
108      * once a mouse key is pressed, other mouse key presses will be disregarded
109      * (except for when the "HOLD" key is pressed).
110      */
111     private int mActiveMoveKey = KEY_NOT_SET;
112 
113     /** State of the active scroll mouse key. */
114     private int mActiveScrollKey = KEY_NOT_SET;
115 
116     /** Last time the key action was performed */
117     private long mLastTimeKeyActionPerformed = 0;
118 
119     /** Whether scroll toggle is on */
120     private boolean mScrollToggleOn = false;
121 
122     /** The ID of the input device that is currently active */
123     private int mActiveInputDeviceId = 0;
124 
125     /**
126      * Enum representing different types of mouse key events, each associated with a specific
127      * key code.
128      *
129      * <p> These events correspond to various mouse actions such as directional movements,
130      * clicks, and scrolls, mapped to specific keys on the keyboard.
131      * The key codes here are the QWERTY key codes, and should be accessed via
132      * {@link MouseKeyEvent#getKeyCode(InputDevice)}
133      * so that it is mapped to the equivalent key on the keyboard layout of the keyboard device
134      * that is actually in use.
135      * </p>
136      */
137     public enum MouseKeyEvent {
138         DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7),
139         UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8),
140         DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_9),
141         LEFT_MOVE_OR_SCROLL(KeyEvent.KEYCODE_U),
142         RIGHT_MOVE_OR_SCROLL(KeyEvent.KEYCODE_O),
143         DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_J),
144         DOWN_MOVE_OR_SCROLL(KeyEvent.KEYCODE_K),
145         DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_L),
146         LEFT_CLICK(KeyEvent.KEYCODE_I),
147         RIGHT_CLICK(KeyEvent.KEYCODE_SLASH),
148         HOLD(KeyEvent.KEYCODE_M),
149         RELEASE(KeyEvent.KEYCODE_COMMA),
150         SCROLL_TOGGLE(KeyEvent.KEYCODE_PERIOD);
151 
152         private final int mLocationKeyCode;
MouseKeyEvent(int enumValue)153         MouseKeyEvent(int enumValue) {
154             mLocationKeyCode = enumValue;
155         }
156 
157         @VisibleForTesting
getKeyCodeValue()158         public final int getKeyCodeValue() {
159             return mLocationKeyCode;
160         }
161 
162         /**
163          * Get the key code associated with the given MouseKeyEvent for the given keyboard
164          * input device, taking into account its layout.
165          * The default is to return the keycode for the default layout (QWERTY).
166          * We check if the input device has been generated using {@link InputDevice#getGeneration()}
167          * to test with the default {@link MouseKeyEvent} values in the unit tests.
168          */
getKeyCode(InputDevice inputDevice)169         public int getKeyCode(InputDevice inputDevice) {
170             if (inputDevice.getGeneration() == -1) {
171                 return mLocationKeyCode;
172             }
173             return inputDevice.getKeyCodeForKeyLocation(mLocationKeyCode);
174         }
175 
176         /**
177          * Convert int value of the key code to corresponding {@link MouseKeyEvent}
178          * enum for a particular device ID.
179          * If no matching value is found, this will return {@code null}.
180          */
181         @Nullable
from(int keyCode, int deviceId, SparseArray<SparseArray<MouseKeyEvent>> deviceKeyCodeMap)182         public static MouseKeyEvent from(int keyCode, int deviceId,
183                 SparseArray<SparseArray<MouseKeyEvent>> deviceKeyCodeMap) {
184             SparseArray<MouseKeyEvent> keyCodeToEnumMap = deviceKeyCodeMap.get(deviceId);
185             if (keyCodeToEnumMap != null) {
186                 return keyCodeToEnumMap.get(keyCode);
187             }
188             return null;
189         }
190     }
191 
192     /**
193      * Create a map of key codes to their corresponding {@link MouseKeyEvent} values
194      * for a specific input device.
195      * The key for {@code mDeviceKeyCodeMap} is the deviceId.
196      * The key for {@code keyCodeToEnumMap} is the keycode for each
197      * {@link MouseKeyEvent} according to the keyboard layout of the input device.
198      */
initializeDeviceToEnumMap(InputDevice inputDevice)199     public void initializeDeviceToEnumMap(InputDevice inputDevice) {
200         int deviceId = inputDevice.getId();
201         SparseArray<MouseKeyEvent> keyCodeToEnumMap = new SparseArray<>();
202         for (MouseKeyEvent mouseKeyEventType : MouseKeyEvent.values()) {
203             int keyCode = mouseKeyEventType.getKeyCode(inputDevice);
204             keyCodeToEnumMap.put(keyCode, mouseKeyEventType);
205         }
206         mDeviceKeyCodeMap.put(deviceId, keyCodeToEnumMap);
207     }
208 
209     /**
210      * Construct a new MouseKeysInterceptor.
211      *
212      * @param service The service to notify of key events
213      * @param looper Looper to use for callbacks and messages
214      * @param displayId Display ID to send mouse events to
215      */
216     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
MouseKeysInterceptor(AccessibilityManagerService service, InputManager inputManager, Looper looper, int displayId)217     public MouseKeysInterceptor(AccessibilityManagerService service,
218             InputManager inputManager, Looper looper, int displayId) {
219         mAms = service;
220         mInputManager = inputManager;
221         mHandler = new Handler(looper, this);
222         // Create the virtual mouse on a separate thread since virtual device creation
223         // should happen on an auxiliary thread, and not from the handler's thread.
224         // This is because the handler thread is the same as the main thread,
225         // and the main thread will be blocked waiting for the virtual device to be created.
226         mCreateVirtualMouseThread = new Thread(() -> {
227             mVirtualMouse = createVirtualMouse(displayId);
228         });
229         mCreateVirtualMouseThread.start();
230         // Register an input device listener to watch when input devices are
231         // added, removed or reconfigured.
232         mInputManager.registerInputDeviceListener(this, mHandler);
233     }
234 
235     /**
236      * Wait for {@code mVirtualMouse} to be created.
237      * This will ensure that {@code mVirtualMouse} is always created before
238      * trying to send mouse events.
239      **/
waitForVirtualMouseCreation()240     private void waitForVirtualMouseCreation() {
241         try {
242             // Block the current thread until the virtual mouse creation thread completes.
243             mCreateVirtualMouseThread.join();
244         } catch (InterruptedException e) {
245             Thread.currentThread().interrupt();
246             throw new RuntimeException(e);
247         }
248     }
249 
250     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
sendVirtualMouseRelativeEvent(float x, float y)251     private void sendVirtualMouseRelativeEvent(float x, float y) {
252         waitForVirtualMouseCreation();
253         mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder()
254                 .setRelativeX(x)
255                 .setRelativeY(y)
256                 .build()
257         );
258     }
259 
260     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
sendVirtualMouseButtonEvent(int buttonCode, int actionCode)261     private void sendVirtualMouseButtonEvent(int buttonCode, int actionCode) {
262         waitForVirtualMouseCreation();
263         mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder()
264                 .setAction(actionCode)
265                 .setButtonCode(buttonCode)
266                 .build()
267         );
268     }
269 
270     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
sendVirtualMouseScrollEvent(float x, float y)271     private void sendVirtualMouseScrollEvent(float x, float y) {
272         waitForVirtualMouseCreation();
273         mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
274                 .setXAxisMovement(x)
275                 .setYAxisMovement(y)
276                 .build()
277         );
278     }
279 
280     /**
281      * Performs a mouse scroll action based on the provided key code.
282      * The scroll action will only be performed if the scroll toggle is on.
283      * This method interprets the key code as a mouse scroll and sends
284      * the corresponding {@code VirtualMouseScrollEvent#mYAxisMovement}.
285 
286      * @param keyCode The key code representing the mouse scroll action.
287      *                Supported keys are:
288      *                <ul>
289      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL}
290      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL}
291      *                </ul>
292      */
293     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
performMouseScrollAction(int keyCode)294     private void performMouseScrollAction(int keyCode) {
295         MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(
296                 keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap);
297         float x = 0f;
298         float y = 0f;
299 
300         switch (mouseKeyEvent) {
301             case UP_MOVE_OR_SCROLL -> {
302                 y = MOUSE_SCROLL_STEP;
303             }
304             case DOWN_MOVE_OR_SCROLL -> {
305                 y = -MOUSE_SCROLL_STEP;
306             }
307             case LEFT_MOVE_OR_SCROLL -> {
308                 x = MOUSE_SCROLL_STEP;
309             }
310             case RIGHT_MOVE_OR_SCROLL -> {
311                 x = -MOUSE_SCROLL_STEP;
312             }
313             default -> {
314                 x = 0.0f;
315                 y = 0.0f;
316             }
317         }
318         sendVirtualMouseScrollEvent(x, y);
319         if (DEBUG) {
320             Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
321                     + " for scroll action with axis movement (x=" + x + ", y=" + y + ")");
322         }
323     }
324 
325     /**
326      * Performs a mouse button action based on the provided key code.
327      * This method interprets the key code as a mouse button press and sends
328      * the corresponding press and release events to the virtual mouse.
329 
330      * @param keyCode The key code representing the mouse button action.
331      *                Supported keys are:
332      *                <ul>
333      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_CLICK} (Primary Button)
334      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_CLICK} (Secondary
335      *                  Button)
336      *                </ul>
337      */
338     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
performMouseButtonAction(int keyCode)339     private void performMouseButtonAction(int keyCode) {
340         MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(
341                 keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap);
342         int buttonCode = switch (mouseKeyEvent) {
343             case LEFT_CLICK -> VirtualMouseButtonEvent.BUTTON_PRIMARY;
344             case RIGHT_CLICK -> VirtualMouseButtonEvent.BUTTON_SECONDARY;
345             default -> VirtualMouseButtonEvent.BUTTON_UNKNOWN;
346         };
347         if (buttonCode != VirtualMouseButtonEvent.BUTTON_UNKNOWN) {
348             sendVirtualMouseButtonEvent(buttonCode,
349                     VirtualMouseButtonEvent.ACTION_BUTTON_PRESS);
350             sendVirtualMouseButtonEvent(buttonCode,
351                     VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE);
352         }
353         if (DEBUG) {
354             if (buttonCode == VirtualMouseButtonEvent.BUTTON_UNKNOWN) {
355                 Slog.d(LOG_TAG, "Button code is unknown for mouse key event: "
356                         + mouseKeyEvent.name());
357             } else {
358                 Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
359                         + " for button action");
360             }
361         }
362     }
363 
364     /**
365      * Performs a mouse pointer action based on the provided key code.
366      * The method calculates the relative movement of the mouse pointer
367      * and sends the corresponding event to the virtual mouse.
368      *
369      * The UP, DOWN, LEFT, RIGHT  pointer actions will only take place for their
370      * respective keys if the scroll toggle is off.
371      *
372      * @param keyCode The key code representing the direction or button press.
373      *                Supported keys are:
374      *                <ul>
375      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_LEFT_MOVE}
376      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL}
377      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_RIGHT_MOVE}
378      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE_OR_SCROLL}
379      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE_OR_SCROLL}
380      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_LEFT_MOVE}
381      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL}
382      *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_RIGHT_MOVE}
383      *                </ul>
384      */
385     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
performMousePointerAction(int keyCode)386     private void performMousePointerAction(int keyCode) {
387         float x = 0f;
388         float y = 0f;
389         MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(
390                 keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap);
391 
392         switch (mouseKeyEvent) {
393             case DIAGONAL_DOWN_LEFT_MOVE -> {
394                 x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
395                 y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
396             }
397             case DOWN_MOVE_OR_SCROLL -> {
398                 if (!mScrollToggleOn) {
399                     y = MOUSE_POINTER_MOVEMENT_STEP;
400                 }
401             }
402             case DIAGONAL_DOWN_RIGHT_MOVE -> {
403                 x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
404                 y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
405             }
406             case LEFT_MOVE_OR_SCROLL -> {
407                 x = -MOUSE_POINTER_MOVEMENT_STEP;
408             }
409             case RIGHT_MOVE_OR_SCROLL -> {
410                 x = MOUSE_POINTER_MOVEMENT_STEP;
411             }
412             case DIAGONAL_UP_LEFT_MOVE -> {
413                 x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
414                 y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
415             }
416             case UP_MOVE_OR_SCROLL -> {
417                 if (!mScrollToggleOn) {
418                     y = -MOUSE_POINTER_MOVEMENT_STEP;
419                 }
420             }
421             case DIAGONAL_UP_RIGHT_MOVE -> {
422                 x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
423                 y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
424             }
425             default -> {
426                 x = 0.0f;
427                 y = 0.0f;
428             }
429         }
430         sendVirtualMouseRelativeEvent(x, y);
431         if (DEBUG) {
432             Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
433                     + " for relative pointer movement (x=" + x + ", y=" + y + ")");
434         }
435     }
436 
isMouseKey(int keyCode, int deviceId)437     private boolean isMouseKey(int keyCode, int deviceId) {
438         SparseArray<MouseKeyEvent> keyCodeToEnumMap = mDeviceKeyCodeMap.get(deviceId);
439         return keyCodeToEnumMap.contains(keyCode);
440     }
441 
isMouseButtonKey(int keyCode, InputDevice inputDevice)442     private boolean isMouseButtonKey(int keyCode, InputDevice inputDevice) {
443         return keyCode == MouseKeyEvent.LEFT_CLICK.getKeyCode(inputDevice)
444                 || keyCode == MouseKeyEvent.RIGHT_CLICK.getKeyCode(inputDevice);
445     }
446 
isMouseScrollKey(int keyCode, InputDevice inputDevice)447     private boolean isMouseScrollKey(int keyCode, InputDevice inputDevice) {
448         return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCode(inputDevice)
449                 || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCode(inputDevice)
450                 || keyCode == MouseKeyEvent.LEFT_MOVE_OR_SCROLL.getKeyCode(inputDevice)
451                 || keyCode == MouseKeyEvent.RIGHT_MOVE_OR_SCROLL.getKeyCode(inputDevice);
452     }
453 
454     /**
455      * Create a virtual mouse using the VirtualDeviceManagerInternal.
456      *
457      * @return The created VirtualMouse.
458      */
459     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
createVirtualMouse(int displayId)460     private VirtualMouse createVirtualMouse(int displayId) {
461         final VirtualDeviceManagerInternal localVdm =
462                 LocalServices.getService(VirtualDeviceManagerInternal.class);
463         mVirtualDevice = localVdm.createVirtualDevice(
464                 new VirtualDeviceParams.Builder().setName("Mouse Keys Virtual Device").build());
465         VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse(
466                 new VirtualMouseConfig.Builder()
467                 .setInputDeviceName("Mouse Keys Virtual Mouse")
468                 .setAssociatedDisplayId(displayId)
469                 .build());
470         return virtualMouse;
471     }
472 
473     /**
474      * Handles key events and forwards mouse key events to the virtual mouse on the handler thread.
475      *
476      * @param event The key event to handle.
477      * @param policyFlags The policy flags associated with the key event.
478      */
479     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
480     @Override
onKeyEvent(KeyEvent event, int policyFlags)481     public void onKeyEvent(KeyEvent event, int policyFlags) {
482         if (mAms.getTraceManager().isA11yTracingEnabledForTypes(FLAGS_INPUT_FILTER)) {
483             mAms.getTraceManager().logTrace(LOG_TAG + ".onKeyEvent",
484                     FLAGS_INPUT_FILTER, "event=" + event + ";policyFlags=" + policyFlags);
485         }
486 
487         mHandler.post(() -> {
488             onKeyEventInternal(event, policyFlags);
489         });
490     }
491 
492     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
onKeyEventInternal(KeyEvent event, int policyFlags)493     private void onKeyEventInternal(KeyEvent event, int policyFlags) {
494         boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;
495         int keyCode = event.getKeyCode();
496         mActiveInputDeviceId = event.getDeviceId();
497         InputDevice inputDevice = mInputManager.getInputDevice(mActiveInputDeviceId);
498 
499         if (!mDeviceKeyCodeMap.contains(mActiveInputDeviceId)) {
500             initializeDeviceToEnumMap(inputDevice);
501         }
502 
503         if (!isMouseKey(keyCode, mActiveInputDeviceId)) {
504             // Pass non-mouse key events to the next handler
505             super.onKeyEvent(event, policyFlags);
506         } else if (isDown) {
507             if (keyCode == MouseKeyEvent.SCROLL_TOGGLE.getKeyCode(inputDevice)) {
508                 mScrollToggleOn = !mScrollToggleOn;
509                 if (DEBUG) {
510                     Slog.d(LOG_TAG, "Scroll toggle " + (mScrollToggleOn ? "ON" : "OFF"));
511                 }
512             } else if (keyCode == MouseKeyEvent.HOLD.getKeyCode(inputDevice)) {
513                 sendVirtualMouseButtonEvent(
514                         VirtualMouseButtonEvent.BUTTON_PRIMARY,
515                         VirtualMouseButtonEvent.ACTION_BUTTON_PRESS
516                 );
517             } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCode(inputDevice)) {
518                 sendVirtualMouseButtonEvent(
519                         VirtualMouseButtonEvent.BUTTON_PRIMARY,
520                         VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE
521                 );
522             } else if (isMouseButtonKey(keyCode, inputDevice)) {
523                 performMouseButtonAction(keyCode);
524             } else if (mScrollToggleOn && isMouseScrollKey(keyCode, inputDevice)) {
525                 // If the scroll key is pressed down and no other key is active,
526                 // set it as the active key and send a message to scroll the pointer
527                 if (mActiveScrollKey == KEY_NOT_SET) {
528                     mActiveScrollKey = keyCode;
529                     mLastTimeKeyActionPerformed = event.getDownTime();
530                     mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER);
531                 }
532             } else {
533                 // This is a directional key.
534                 // If the key is pressed down and no other key is active,
535                 // set it as the active key and send a message to move the pointer
536                 if (mActiveMoveKey == KEY_NOT_SET) {
537                     mActiveMoveKey = keyCode;
538                     mLastTimeKeyActionPerformed = event.getDownTime();
539                     mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER);
540                 }
541             }
542         } else {
543             // Up event received
544             if (mActiveMoveKey == keyCode) {
545                 // If the key is released, and it is the active key, stop moving the pointer
546                 mActiveMoveKey = KEY_NOT_SET;
547                 mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER);
548             } else if (mActiveScrollKey == keyCode) {
549                 // If the key is released, and it is the active key, stop scrolling the pointer
550                 mActiveScrollKey = KEY_NOT_SET;
551                 mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER);
552             } else {
553                 Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode
554                         + "', with no matching down event from deviceId = "
555                         + event.getDeviceId());
556             }
557         }
558     }
559 
560     /**
561      * Handle messages for moving or scrolling the mouse pointer.
562      *
563      * @param msg The message to handle.
564      * @return True if the message was handled, false otherwise.
565      */
566     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
567     @Override
handleMessage(Message msg)568     public boolean handleMessage(Message msg) {
569         switch (msg.what) {
570             case MESSAGE_MOVE_MOUSE_POINTER ->
571                     handleMouseMessage(msg.getWhen(), mActiveMoveKey, MESSAGE_MOVE_MOUSE_POINTER);
572             case MESSAGE_SCROLL_MOUSE_POINTER ->
573                     handleMouseMessage(msg.getWhen(), mActiveScrollKey,
574                             MESSAGE_SCROLL_MOUSE_POINTER);
575             default -> {
576                 Slog.e(LOG_TAG, "Unexpected message type");
577                 return false;
578             }
579         }
580         return true;
581     }
582 
583     /**
584      * Handles mouse-related messages for moving or scrolling the mouse pointer.
585      * This method checks if the specified time interval {@code INTERVAL_MILLIS} has passed since
586      * the last movement or scroll action and performs the corresponding action if necessary.
587      * If there is an active key, the message is rescheduled to be handled again
588      * after the specified {@code INTERVAL_MILLIS}.
589      *
590      * @param currentTime The current time when the message is being handled.
591      * @param activeKey The key code representing the active key. This determines
592      *                  the direction or type of action to be performed.
593      * @param messageType The type of message to be handled. It can be one of the
594      *                    following:
595      *                    <ul>
596      *                      <li>{@link #MESSAGE_MOVE_MOUSE_POINTER} - for moving the mouse pointer.
597      *                      <li>{@link #MESSAGE_SCROLL_MOUSE_POINTER} - for scrolling mouse pointer.
598      *                    </ul>
599      */
600     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
handleMouseMessage(long currentTime, int activeKey, int messageType)601     public void handleMouseMessage(long currentTime, int activeKey, int messageType) {
602         if (currentTime - mLastTimeKeyActionPerformed >= INTERVAL_MILLIS) {
603             if (messageType == MESSAGE_MOVE_MOUSE_POINTER) {
604                 performMousePointerAction(activeKey);
605             } else if (messageType == MESSAGE_SCROLL_MOUSE_POINTER) {
606                 performMouseScrollAction(activeKey);
607             }
608             mLastTimeKeyActionPerformed = currentTime;
609         }
610         if (activeKey != KEY_NOT_SET) {
611             // Reschedule the message if the key is still active
612             mHandler.sendEmptyMessageDelayed(messageType, INTERVAL_MILLIS);
613         }
614     }
615 
616     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
617     @Override
onDestroy()618     public void onDestroy() {
619         mHandler.post(() -> {
620             // Clear mouse state
621             mActiveMoveKey = KEY_NOT_SET;
622             mActiveScrollKey = KEY_NOT_SET;
623             mLastTimeKeyActionPerformed = 0;
624             mDeviceKeyCodeMap.clear();
625         });
626 
627         mHandler.removeCallbacksAndMessages(null);
628         if (mVirtualDevice != null) {
629             mVirtualDevice.close();
630         }
631     }
632 
633     @Override
onInputDeviceAdded(int deviceId)634     public void onInputDeviceAdded(int deviceId) {
635     }
636 
637     @Override
onInputDeviceRemoved(int deviceId)638     public void onInputDeviceRemoved(int deviceId) {
639         mDeviceKeyCodeMap.remove(deviceId);
640     }
641 
642     /**
643      * The user can change the keyboard layout from settings at anytime, which would change
644      * key character map for that device. Hence, we should use this callback to
645      * update the key code to enum mapping if there is a change in the physical keyboard detected.
646      *
647      * @param deviceId The id of the input device that changed.
648      */
649     @Override
onInputDeviceChanged(int deviceId)650     public void onInputDeviceChanged(int deviceId) {
651         InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
652         // Update the enum mapping only if input device that changed is a keyboard
653         if (inputDevice.isFullKeyboard() && !mDeviceKeyCodeMap.contains(deviceId)) {
654             initializeDeviceToEnumMap(inputDevice);
655         }
656     }
657 }
658