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 instance for injecting events and accessing screen state
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)
MotionEvent action constant (e.g., ACTION_DOWN, ACTION_MOVE, ACTION_UP)
Unique identifier for the touch pointer (0-9 for multi-touch)
Touch coordinates on the screen
Returns true if the event was successfully injected
Supported Actions:
First finger touches the screen
Last finger leaves the screen
Additional finger touches (for multi-touch)
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)
Android KeyEvent keycode constant
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)
KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP
Repeat count for the key event
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);
}
Events are received as integer arrays from the client.
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};
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
Proximity sensor near - Sets proximity flag to true
Proximity sensor far - Sets proximity flag to false
Multi-Touch Support
The EventController supports up to 10 simultaneous touch points.
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)
The MotionEvent or KeyEvent to inject
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.
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();
}
}
}
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