Skip to main content

Overview

The EventController class runs on the scrcpy server side and processes input events received from the client, injecting them into the Android system. It supports multi-touch gestures, keyboard input, and screen power management. Package: org.server.scrcpy Location: Server-side (runs on the Android device being controlled)

Initialization

Constructor

Creates a new EventController instance.
public EventController(Device device, DroidConnection connection)
device
Device
required
Device instance for injecting events and accessing screen state
connection
DroidConnection
required
Connection instance for receiving control events from the client
Example:
Device device = new Device();
DroidConnection connection = new DroidConnection(socket);
EventController controller = new EventController(device, connection);

Main Control Loop

control()

Main event processing loop that receives and handles input events.
public void control() throws IOException
Behavior:
  • Turns the screen on when starting
  • Continuously receives events from the client
  • Routes events to appropriate handlers
  • Blocks until an IOException occurs
Example:
try {
    controller.control();
} catch (IOException e) {
    Log.e("EventController", "Connection lost: " + e.getMessage());
}
Event Loop Implementation:
// From source (EventController.java:66-121)
public void control() throws IOException {
    // Turn screen on at start
    turnScreenOn();
    
    while (true) {
        int[] buffer = connection.NewreceiveControlEvent();
        if (buffer != null) {
            long now = SystemClock.uptimeMillis();
            
            // Check if this is a keycode event (no coordinates)
            if (buffer[2] == 0 && buffer[3] == 0) {
                if (buffer[0] == 28) {
                    proximity = true;  // Proximity sensor near
                } else if (buffer[0] == 29) {
                    proximity = false; // Proximity sensor far
                } else {
                    injectKeycode(buffer[0]);
                }
            } else {
                // Touch event with coordinates
                Point point = new Point(buffer[2], buffer[3]);
                Point newpoint = device.NewgetPhysicalPoint(point);
                injectTouch(buffer[0], buffer[4], newpoint, buffer[1]);
            }
        }
    }
}

Touch Event Handling

injectTouch()

Processes and injects touch events with multi-touch support.
private boolean injectTouch(int action, long pointerId, Point point, int button)
action
int
required
MotionEvent action constant (e.g., ACTION_DOWN, ACTION_MOVE, ACTION_UP)
pointerId
long
required
Unique identifier for the touch pointer (0-9 for multi-touch)
point
Point
required
Touch coordinates on the screen
button
int
required
Button state flags
return
boolean
Returns true if the event was successfully injected
Supported Actions:
ACTION_DOWN
int
First finger touches the screen
ACTION_UP
int
Last finger leaves the screen
ACTION_MOVE
int
Touch point moves
ACTION_POINTER_DOWN
int
Additional finger touches (for multi-touch)
ACTION_POINTER_UP
int
Additional finger leaves (for multi-touch)
Multi-Touch Implementation:
// From source (EventController.java:158-183)
private boolean injectTouch(int action, long pointerId, Point point, int button) {
    long now = SystemClock.uptimeMillis();
    
    // Get or create pointer index
    int pointerIndex = pointersState.getPointerIndex(pointerId);
    if (pointerIndex == -1) {
        Ln.w("Too many pointers for touch event");
        return false;
    }
    
    // Update pointer state
    Pointer pointer = pointersState.get(pointerIndex);
    pointer.setPoint(point);
    pointer.setPressure(1.0f);
    
    // Configure as touchscreen event
    pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
    int source = InputDevice.SOURCE_TOUCHSCREEN;
    
    // Handle pointer up events
    boolean pointerUp = action == MotionEvent.ACTION_UP;
    int actionType = action & MotionEvent.ACTION_MASK;
    if (actionType == MotionEvent.ACTION_POINTER_UP) {
        pointerUp = true;
    }
    pointer.setUp(pointerUp);
    
    // Update pointer count and coordinates
    int pointerCount = pointersState.update(pointerProperties, pointerCoords);
    
    // Adjust action for multi-touch
    if (pointerCount == 1) {
        if (action == MotionEvent.ACTION_DOWN) {
            lastMouseDown = now;
        }
    } else {
        // Convert to ACTION_POINTER_* for secondary fingers
        if (action == MotionEvent.ACTION_UP || 
            actionType == MotionEvent.ACTION_POINTER_UP) {
            action = MotionEvent.ACTION_POINTER_UP | 
                    (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
        } else if (action == MotionEvent.ACTION_DOWN || 
                   actionType == MotionEvent.ACTION_POINTER_DOWN) {
            action = MotionEvent.ACTION_POINTER_DOWN | 
                    (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
        }
    }
    
    // Create and inject motion event
    MotionEvent event = MotionEvent.obtain(
        lastMouseDown, now, action, pointerCount,
        pointerProperties, pointerCoords,
        0, button, 1f, 1f, 0, 0, source, 0
    );
    
    return injectEvent(event);
}

Key Event Handling

injectKeycode()

Injects a keycode as both DOWN and UP events.
private boolean injectKeycode(int keyCode)
keyCode
int
required
Android KeyEvent keycode constant
return
boolean
Returns true if both events were successfully injected
Common Keycodes:
  • KeyEvent.KEYCODE_POWER - Power button
  • KeyEvent.KEYCODE_BACK - Back button
  • KeyEvent.KEYCODE_HOME - Home button
  • KeyEvent.KEYCODE_VOLUME_UP - Volume up
  • KeyEvent.KEYCODE_VOLUME_DOWN - Volume down
Implementation:
// From source (EventController.java:256-258)
private boolean injectKeycode(int keyCode) {
    return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
        && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}

injectKeyEvent()

Injects a single key event with full control.
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState)
action
int
required
KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP
keyCode
int
required
Android KeyEvent keycode
repeat
int
required
Repeat count for the key event
metaState
int
required
Meta key state (Shift, Alt, Ctrl, etc.)
Implementation:
// From source (EventController.java:249-253)
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
    long now = SystemClock.uptimeMillis();
    KeyEvent event = new KeyEvent(
        now, now, action, keyCode, repeat, metaState,
        KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
        InputDevice.SOURCE_KEYBOARD
    );
    return injectEvent(event);
}

Event Data Format

Events are received as integer arrays from the client.

Touch Event Format

Array Structure (5 integers = 20 bytes):
buffer[0] = action;      // MotionEvent action constant
buffer[1] = buttonState; // Button state flags
buffer[2] = x;           // X coordinate
buffer[3] = y;           // Y coordinate
buffer[4] = pointerId;   // Pointer ID for multi-touch (0-9)
Example:
// Single finger down at (500, 800)
int[] touchDown = {MotionEvent.ACTION_DOWN, 0, 500, 800, 0};

// Second finger down at (600, 900)
int[] secondFinger = {MotionEvent.ACTION_POINTER_DOWN, 0, 600, 900, 1};

// Move first finger to (510, 810)
int[] move = {MotionEvent.ACTION_MOVE, 0, 510, 810, 0};

Key Event Format

Array Structure (1 integer = 4 bytes):
buffer[0] = keyCode; // Android keycode
buffer[1] = 0;       // Unused
buffer[2] = 0;       // Marks as key event (not touch)
buffer[3] = 0;       // Marks as key event (not touch)
Detection Logic:
if (buffer[2] == 0 && buffer[3] == 0) {
    // This is a key event
    injectKeycode(buffer[0]);
} else {
    // This is a touch event
    injectTouch(buffer[0], buffer[4], 
               new Point(buffer[2], buffer[3]), 
               buffer[1]);
}

Special Keycodes

28
int
Proximity sensor near - Sets proximity flag to true
29
int
Proximity sensor far - Sets proximity flag to false

Multi-Touch Support

The EventController supports up to 10 simultaneous touch points.

PointersState

Manages the state of all active touch pointers.
private final PointersState pointersState = new PointersState();
private final MotionEvent.PointerProperties[] pointerProperties = 
    new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = 
    new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
Pointer Initialization:
// From source (EventController.java:52-64)
private void initPointers() {
    for (int i = 0; i < PointersState.MAX_POINTERS; ++i) {
        MotionEvent.PointerProperties props = 
            new MotionEvent.PointerProperties();
        props.toolType = MotionEvent.TOOL_TYPE_FINGER;
        
        MotionEvent.PointerCoords coords = 
            new MotionEvent.PointerCoords();
        coords.orientation = 0;
        coords.size = 0;
        
        pointerProperties[i] = props;
        pointerCoords[i] = coords;
    }
}

Pointer Tracking Example

// Two-finger pinch gesture

// 1. First finger down
int[] finger1Down = {MotionEvent.ACTION_DOWN, 0, 400, 600, 0};
controller.processEvent(finger1Down);

// 2. Second finger down
int[] finger2Down = {MotionEvent.ACTION_POINTER_DOWN, 0, 600, 800, 1};
controller.processEvent(finger2Down);

// 3. Move both fingers (pinch in)
int[] finger1Move = {MotionEvent.ACTION_MOVE, 0, 450, 650, 0};
int[] finger2Move = {MotionEvent.ACTION_MOVE, 0, 550, 750, 1};
controller.processEvent(finger1Move);
controller.processEvent(finger2Move);

// 4. First finger up
int[] finger1Up = {MotionEvent.ACTION_POINTER_UP, 0, 450, 650, 0};
controller.processEvent(finger1Up);

// 5. Second finger up
int[] finger2Up = {MotionEvent.ACTION_UP, 0, 550, 750, 1};
controller.processEvent(finger2Up);

Screen Power Management

The EventController includes special handling for screen wake-up.

Double-Tap Wake

When the screen is off or proximity sensor is active, tapping twice quickly wakes the screen:
// From source (EventController.java:85-97)
int action = buffer[0];
if (action == MotionEvent.ACTION_UP && 
    (!device.isScreenOn() || proximity)) {
    
    if (hit) {
        // Second tap within 250ms
        if (now - then < 250) {
            then = 0;
            hit = false;
            injectKeycode(KeyEvent.KEYCODE_POWER); // Wake screen
        } else {
            then = now;
        }
    } else {
        // First tap
        hit = true;
        then = now;
    }
}

turnScreenOn()

Ensures the screen is on when the controller starts.
private boolean turnScreenOn()
Implementation:
// From source (EventController.java:265-267)
private boolean turnScreenOn() {
    return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
}

Event Injection

injectEvent()

Low-level method to inject events into the Android system.
private boolean injectEvent(InputEvent event)
event
InputEvent
required
The MotionEvent or KeyEvent to inject
return
boolean
Returns true if injection was successful
Implementation:
// From source (EventController.java:261-263)
private boolean injectEvent(InputEvent event) {
    return device.injectInputEvent(
        event, 
        InputManager.INJECT_INPUT_EVENT_MODE_ASYNC
    );
}
Events are injected asynchronously (INJECT_INPUT_EVENT_MODE_ASYNC) for better performance and to avoid blocking the control loop.

Coordinate Transformation

Touch coordinates are transformed from client screen space to device physical coordinates:
Point point = new Point(buffer[2], buffer[3]);        // Client coordinates
Point newpoint = device.NewgetPhysicalPoint(point);   // Physical coordinates
injectTouch(action, pointerId, newpoint, buttonState);
This handles:
  • Screen rotation
  • Resolution differences
  • Aspect ratio adjustments

Usage Example

Complete server-side event handling setup:
public class ScrcpyServer {
    
    public void startServer(Socket socket) throws IOException {
        // Initialize device and connection
        Device device = new Device();
        DroidConnection connection = new DroidConnection(socket);
        
        // Create event controller
        EventController controller = new EventController(device, connection);
        
        // Start control loop (blocks until error)
        try {
            controller.control();
        } catch (IOException e) {
            Log.e("Server", "Control loop ended: " + e.getMessage());
        } finally {
            socket.close();
        }
    }
}

Performance Considerations

Event injection requires system-level permissions. The server must run with appropriate privileges to inject input events.
Key Points:
  • Events are processed sequentially in the order received
  • Asynchronous injection prevents blocking
  • Multi-touch state is tracked automatically
  • Coordinate transformation is applied per-event
  • Screen state affects touch event handling

See Also

Build docs developers (and LLMs) love