How scrcpy Injects Keyboard and Mouse Events into Android: A Deep Dive into the Control Pipeline

scrcpy injects keyboard and mouse events by transmitting control messages from the desktop client to an Android server that constructs KeyEvent and MotionEvent objects, then uses Java reflection to invoke the hidden InputManager.injectInputEvent() method.

The Genymobile/scrcpy project achieves seamless desktop-to-Android input forwarding without requiring root access by running a privileged server component on the device. This article examines the three-layer pipeline that enables scrcpy to inject keyboard and mouse events into Android, referencing the actual source implementation.

The Three-Layer Injection Architecture

scrcpy’s event injection operates through a distinct separation of concerns across the control channel, device abstraction, and system service layers.

Control-Channel Handling (Controller.java)

The desktop client sends binary control messages over a socket to the Android server. The Controller class, located at server/src/main/java/com/genymobile/scrcpy/control/Controller.java, parses these messages and constructs the appropriate Android input objects.

Key methods in this layer include:

  • injectKeycode() – Handles physical key presses (e.g., HOME, BACK)
  • injectChar() – Processes Unicode character input
  • injectTouch() – Translates mouse coordinates to touch events
  • injectScroll() – Handles scroll wheel deltas

The Controller converts raw protocol messages into KeyEvent or MotionEvent instances, then forwards them to the device layer for injection.

Device-Level Facade (Device.java)

The Device class at server/src/main/java/com/genymobile/scrcpy/device/Device.java acts as a façade that prepares events for the Android system. It handles display-specific routing and event metadata.

For keyboard input, Device.injectKeyEvent() creates a KeyEvent with the proper source flag (InputDevice.SOURCE_KEYBOARD) and calls the generic Device.injectEvent() method.

For mouse and touch input, Device.injectEvent() receives the InputEvent and performs critical preprocessing:

  1. Display binding: If the target display is not the default, it attaches the display ID via InputManager.setDisplayId()
  2. Button state: For mouse events, it sets the action button via InputManager.setActionButton()
  3. Source attribution: Ensures the event carries the correct input source (mouse, touchpad, or keyboard)

Android System Injection (InputManager Wrapper)

The final layer involves calling the hidden Android API. scrcpy cannot directly invoke android.hardware.input.InputManager.injectInputEvent() because it is marked @hide, so it uses reflection via the wrapper class at server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java.

The wrapper performs the following:

  • Lazy resolution: Caches the Method object for injectInputEvent(InputEvent, int) on first use
  • Service retrieval: Obtains the system service via ServiceManager.getInputManager() (from server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java)
  • Field injection: Uses reflection to set private fields on the event object:
    • setDisplayId() for multi-display support
    • setActionButton() for mouse button tracking
  • Error handling: Catches SecurityException for missing INJECT_EVENTS permission and logs appropriately

The injection modes—INJECT_INPUT_EVENT_MODE_ASYNC, WAIT_FOR_RESULT, and WAIT_FOR_FINISH—are defined as constants in the wrapper and passed through Device to control whether the call blocks until the event is processed.

Event Types and Processing Details

Keyboard Event Composition

For text input, scrcpy handles character composition through KeyComposition (located in the control package). When injectChar() receives a Unicode character, it uses KeyCharacterMap.getEvents() to decompose the character into a sequence of key codes. This ensures that complex characters requiring modifier keys (e.g., accented letters) generate the correct KeyEvent sequence.

Mouse and Touch Translation

Mouse events undergo coordinate transformation from desktop screen space to Android display coordinates. The Controller maintains pointer IDs to support multi-touch scenarios. When injecting:

  • MotionEvent objects are created with MotionEvent.obtain() using the appropriate action (ACTION_DOWN, ACTION_MOVE, ACTION_UP)
  • Button state is preserved via setActionButton() to distinguish primary, secondary, and tertiary clicks
  • Scroll events are converted from desktop scroll deltas to Android MotionEvent axis values

Practical Implementation Examples

Injecting a Keycode (Home Button)

To programmatically inject a Home key press using scrcpy’s server classes:

// Inject HOME key on the primary display
boolean success = Device.injectKeyEvent(
    KeyEvent.ACTION_DOWN,
    KeyEvent.KEYCODE_HOME,
    0,                          // repeat count
    0,                          // meta state
    Device.DISPLAY_ID_NONE,     // default display
    Device.INJECT_MODE_ASYNC    // non-blocking
);
success &= Device.injectKeyEvent(
    KeyEvent.ACTION_UP,
    KeyEvent.KEYCODE_HOME,
    0, 0, 
    Device.DISPLAY_ID_NONE, 
    Device.INJECT_MODE_ASYNC
);

This uses the Device.injectKeyEvent() pathway that creates a properly sourced KeyEvent and routes it through the reflective InputManager wrapper.

Simulating a Mouse Click

To simulate a primary mouse click at specific coordinates on a secondary display:

long now = SystemClock.uptimeMillis();
float x = 400f, y = 300f;
int displayId = 1;
int button = MotionEvent.BUTTON_PRIMARY;

// ACTION_DOWN
MotionEvent down = MotionEvent.obtain(
    now, now, MotionEvent.ACTION_DOWN, x, y, 1.0f);
down.setSource(InputDevice.SOURCE_MOUSE);
InputManager.setActionButton(down, button);
Device.injectEvent(down, displayId, Device.INJECT_MODE_ASYNC);

// ACTION_UP
MotionEvent up = MotionEvent.obtain(
    now, now, MotionEvent.ACTION_UP, x, y, 0);
up.setSource(InputDevice.SOURCE_MOUSE);
InputManager.setActionButton(up, button);
Device.injectEvent(up, displayId, Device.INJECT_MODE_ASYNC);

This demonstrates the display-specific routing and button state preservation required for accurate mouse injection.

Sending Text Input

To inject a text string character-by-character:

String text = "scrcpy";
for (char c : text.toCharArray()) {
    // Decompose character to key events using KeyCharacterMap
    KeyEvent[] events = KeyCharacterMap.VIRTUAL_KEYBOARD.getEvents(new char[]{c});
    if (events != null) {
        for (KeyEvent event : events) {
            Device.injectKeyEvent(
                event.getAction(),
                event.getKeyCode(),
                event.getRepeatCount(),
                event.getMetaState(),
                Device.DISPLAY_ID_NONE,
                Device.INJECT_MODE_ASYNC
            );
        }
    }
}

This utilizes the KeyCharacterMap system to handle complex character composition, ensuring proper handling of modifiers and dead keys.

Key Source Files and Architecture

The injection pipeline spans several critical files in the scrcpy server:

Component File Path Primary Responsibility
Control Message Parser server/src/main/java/com/genymobile/scrcpy/control/Controller.java Parses socket messages and constructs KeyEvent/MotionEvent objects via injectKeycode(), injectChar(), injectTouch(), and injectScroll()
Device Abstraction server/src/main/java/com/genymobile/scrcpy/device/Device.java Provides injectKeyEvent() and injectEvent() to attach display IDs and route to the system service
System Service Wrapper server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java Reflectively invokes injectInputEvent(), handles multi-display fields, and manages injection modes
Service Locator server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java Provides singleton access to the system InputManager instance
Text Composition server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java Converts Unicode characters to key sequences using KeyCharacterMap

Summary

  • scrcpy injects keyboard and mouse events through a three-layer pipeline: control message parsing (Controller.java), device abstraction (Device.java), and reflective system service access (InputManager.java).
  • The desktop client sends binary control messages over a socket, which the Android server converts into native KeyEvent and MotionEvent objects.
  • Reflection is required to access the hidden InputManager.injectInputEvent() API, as the INJECT_EVENTS permission is not granted to regular apps but is available to shell-level processes started via ADB.
  • Multi-display support is achieved by attaching display IDs to events before injection, allowing mouse and keyboard input to target specific virtual displays.
  • Text input uses KeyCharacterMap to decompose Unicode characters into proper key sequences, handling complex input methods and modifier keys.

Frequently Asked Questions

How does scrcpy inject events without root access?

scrcpy does not require root because it runs a server process on the Android device with shell-level privileges via ADB. When you start scrcpy, it pushes the scrcpy-server binary to /data/local/tmp and executes it through adb shell. This shell context grants the INJECT_EVENTS permission, which allows the server to call the hidden InputManager.injectInputEvent() method through reflection, something regular third-party apps cannot do.

What is the difference between INJECT_MODE_ASYNC and other injection modes?

scrcpy supports three injection modes defined in InputManager.java: INJECT_INPUT_EVENT_MODE_ASYNC, WAIT_FOR_RESULT, and WAIT_FOR_FINISH. INJECT_MODE_ASYNC (the default) returns immediately after queuing the event without waiting for the system to process it. WAIT_FOR_RESULT blocks until the input dispatcher accepts the event, while WAIT_FOR_FINISH waits until the event has been fully processed by the target application. scrcpy typically uses asynchronous mode to minimize latency between desktop input and Android response.

Can scrcpy inject events to specific Android displays?

Yes, scrcpy supports multi-display injection by attaching a display ID to events before they reach the system InputManager. In Device.java, the injectEvent() method checks if the target display differs from the default. If so, it uses reflection to call InputManager.setDisplayId() on the event object. This allows the input to appear on virtual displays or secondary screens rather than the primary touchscreen, essential for scenarios like controlling a device with multiple monitors or DeX mode.

Why does scrcpy use reflection to access InputManager?

scrcpy uses reflection because InputManager.injectInputEvent() is a hidden API marked with @hide in the Android SDK. This method is not exposed to regular applications and requires the INJECT_EVENTS system permission, which is only granted to system apps or shell processes. Since scrcpy runs as a shell process via ADB, it has the necessary permission, but it must still access the method through Java reflection to bypass the SDK visibility restrictions. The wrapper class in InputManager.java caches the reflected method for performance and handles field injection for display IDs and action buttons.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →