This page covers the server-side handling of control messages: how the Android server deserializes binary messages received from the client, dispatches them to the appropriate handler, and executes device actions (input injection, clipboard operations, display power, app launch, and UHID device management). It also covers CleanUp.java, which restores device settings after the session ends.
For the client-side binary wire format, serialization logic, and the outbound send queue, see the Control Message Protocol page (2.8).
The server receives control messages over the control socket, deserializes them, and dispatches them to action methods via Controller.java. Outbound messages (e.g., clipboard sync acknowledgements) are sent back through DeviceMessageSender.
Control message execution data flow
Sources: server/src/main/java/com/genymobile/scrcpy/control/Controller.java1-757 server/src/main/java/com/genymobile/scrcpy/device/Device.java1-315
ControlMessage is the server-side counterpart to the client's sc_control_msg struct. It is a plain data holder for all message types. Each type constant (TYPE_INJECT_KEYCODE, TYPE_INJECT_TEXT, etc.) mirrors the client-side sc_control_msg_type enum exactly by numeric value.
ControlMessageReader is responsible for reading raw bytes from the control socket and parsing them into ControlMessage instances. The reading is done inside ControlChannel, which wraps the socket and exposes a recv() method. Controller calls controlChannel.recv() in a blocking loop.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java258-265
Controller is the central class for server-side input handling. It implements AsyncProcessor and VirtualDisplayListener.
Controller.start() spawns two threads:
| Thread | Name | Role |
|---|---|---|
| Control receive thread | "control-recv" | Blocking loop calling handleEvent() |
| Sender thread | (managed by DeviceMessageSender) | Sends outbound device messages |
server/src/main/java/com/genymobile/scrcpy/control/Controller.java224-256
On startup, if the powerOn option is set and the screen is off, Controller injects a KEYCODE_POWER press and sleeps 500 ms before entering the message loop. This delay prevents race conditions where a display-off message from the client arrives before the device has finished waking up.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java202-221
Controller tracks two display IDs:
displayId: the physical or logical display being mirrored (from --display-id, or DISPLAY_ID_NONE for --new-display).virtualDisplayId: the display created for mirroring, reported asynchronously via onNewVirtualDisplay().The rule for which ID to use for event injection:
| Event type | Display ID used |
|---|---|
| Key events (no coordinates) | displayId (or virtualDisplayId if displayId == DISPLAY_ID_NONE) |
| Positional events (touch, scroll) | virtualDisplayId (from DisplayData) |
server/src/main/java/com/genymobile/scrcpy/control/Controller.java39-64 server/src/main/java/com/genymobile/scrcpy/control/Controller.java628-642
handleEvent() is the main dispatch method. It calls controlChannel.recv(), then switches on msg.getType().
Dispatch table
Sources: server/src/main/java/com/genymobile/scrcpy/control/Controller.java258-339
Note: Input injection messages (
INJECT_KEYCODE,INJECT_TEXT,INJECT_TOUCH_EVENT,INJECT_SCROLL_EVENT,BACK_OR_SCREEN_ON,SET_DISPLAY_POWER) are silently skipped whensupportsInputEventsisfalse. This happens for secondary displays before Android 10.
injectKeycode() calls Device.injectKeyEvent(), which constructs a KeyEvent with KeyCharacterMap.VIRTUAL_KEYBOARD as the device ID and dispatches it via InputManager.injectInputEvent().
Special case: if keepDisplayPowerOff is set and the key is KEYCODE_POWER or KEYCODE_WAKEUP on ACTION_UP, scheduleDisplayPowerOff() is called to re-turn the display off after a 200 ms delay using a ScheduledExecutorService.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java341-347 server/src/main/java/com/genymobile/scrcpy/control/Controller.java547-552
injectText() iterates over each character, calls injectChar(), which calls KeyComposition.decompose() for special characters and then uses KeyCharacterMap.getEvents() to generate the corresponding KeyEvent sequence.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java349-376
injectTouch() performs coordinate mapping from the client's screen space to the device's display space via PositionMapper.map(). It manages a PointersState to track up to PointersState.MAX_POINTERS simultaneous pointers, determining the correct ACTION_POINTER_DOWN / ACTION_POINTER_UP encoding for multi-touch.
Mouse events are distinguished from finger events by POINTER_ID_MOUSE (-1). On API >= 23, mouse button press/release generates additional ACTION_BUTTON_PRESS / ACTION_BUTTON_RELEASE events using InputManager.setActionButton().
server/src/main/java/com/genymobile/scrcpy/control/Controller.java408-516
injectScroll() constructs a MotionEvent with ACTION_SCROLL, setting AXIS_HSCROLL and AXIS_VSCROLL on the pointer coordinates.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java519-542
pressBackOrTurnScreenOn() checks the screen state:
KEYCODE_BACK.action == ACTION_DOWN: inject KEYCODE_POWER (press + release), and re-schedule a power-off if keepDisplayPowerOff is set.server/src/main/java/com/genymobile/scrcpy/control/Controller.java554-571
getClipboard() handles two sub-operations in sequence:
copyKey is specified (COPY_KEY_COPY or COPY_KEY_CUT) and Android >= 7, inject KEYCODE_COPY or KEYCODE_CUT in INJECT_MODE_WAIT_FOR_FINISH to ensure the clipboard is updated before reading.clipboardAutosync is disabled, read the current clipboard via Device.getClipboardText() and send a DeviceMessage.createClipboard(text) back to the client.server/src/main/java/com/genymobile/scrcpy/control/Controller.java573-591
setClipboard() uses isSettingClipboard (an AtomicBoolean) to suppress the clipboard change listener during the operation, preventing an echo back to the client. If paste == true and Android >= 7, it also injects KEYCODE_PASTE. If the message has a valid sequence number, it sends DeviceMessage.createAckClipboard(sequence) back to the client.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java593-613
When clipboardAutosync is enabled, Controller registers a ClipboardManager.OnPrimaryClipChangedListener in its constructor. Any clipboard change not caused by setClipboard() (checked via isSettingClipboard) is forwarded to the client as a DeviceMessage.createClipboard(text).
server/src/main/java/com/genymobile/scrcpy/control/Controller.java119-136
setDisplayPower() calls Device.setDisplayPower(), which uses SurfaceControl.setDisplayPowerMode() for older Android versions, or iterates over all physical display IDs on Android >= 10. On Android >= 15, it can use DisplayManager.requestDisplayPower() (currently disabled due to a known issue).
When the display is turned off, keepDisplayPowerOff is set to true and CleanUp.setRestoreDisplayPower() is called to schedule display power restoration on exit.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java736-749 server/src/main/java/com/genymobile/scrcpy/device/Device.java131-179
Device is a non-instantiable utility class. It is the primary bridge between Controller and the Android framework wrappers.
Key methods in Device.java
| Method | Description |
|---|---|
injectEvent(InputEvent, displayId, injectMode) | Calls InputManager.injectInputEvent() after optionally setting the display ID on the event |
injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode) | Constructs a KeyEvent from a virtual keyboard and calls injectEvent() |
pressReleaseKeycode(keyCode, displayId, injectMode) | Sends key down then key up |
getClipboardText() | Reads from ClipboardManager |
setClipboardText(String) | Writes to ClipboardManager; skips if text is unchanged |
setDisplayPower(displayId, on) | Sets display power mode via SurfaceControl |
expandNotificationPanel() | Calls StatusBarManager.expandNotificationsPanel() |
expandSettingsPanel() | Calls StatusBarManager.expandSettingsPanel() |
collapsePanels() | Calls StatusBarManager.collapsePanels() |
rotateDevice(displayId) | Toggles rotation via WindowManager.freezeRotation() |
startApp(packageName, displayId, forceStop) | Resolves launch intent via PackageManager, launches via ActivityManager |
findByPackageName(String) | Searches installed apps by package name |
findByName(String) | Searches launchable apps by display name prefix |
Sources: server/src/main/java/com/genymobile/scrcpy/device/Device.java51-314
startAppAsync() submits startApp() to a single-threaded ExecutorService (startAppExecutor) because app name resolution can be slow.
startApp() supports two name lookup modes:
Device.findByPackageName(name)?): Device.findByName(name) — lists all launchable apps and matches by name prefix (case-insensitive)A + prefix causes ActivityManager.forceStopPackage() before launching.
The target displayId for launching is resolved via getStartAppDisplayId(), which waits up to 1 second for a virtual display ID if displayId == DISPLAY_ID_NONE.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java644-714 server/src/main/java/com/genymobile/scrcpy/device/Device.java291-314
UhidManager manages virtual HID devices created on the Android device by the UHID kernel subsystem. It is lazily instantiated on the first UHID_CREATE message via getUhidManager().
On Android >= 15 with --new-display, getUhidManager() waits up to 1 second for the virtual display ID before constructing UhidManager, so that the mouse pointer can be associated with the correct display via its unique ID.
| Method | Effect |
|---|---|
UhidManager.open(id, vendorId, productId, name, reportDesc) | Creates a new virtual UHID device |
UhidManager.writeInput(id, data) | Sends an HID input report to the device |
UhidManager.close(id) | Destroys the UHID device |
UhidManager.closeAll() | Called on controller shutdown to clean up all open UHID devices |
server/src/main/java/com/genymobile/scrcpy/control/Controller.java155-186 server/src/main/java/com/genymobile/scrcpy/control/Controller.java230-238
TYPE_RESET_VIDEO calls surfaceCapture.requestInvalidate(), which signals the video encoder to force a new I-frame. The SurfaceCapture reference is injected into Controller via setSurfaceCapture() after construction.
server/src/main/java/com/genymobile/scrcpy/control/Controller.java751-756
CleanUp is a separate class that ensures device state is restored even if the scrcpy server process is killed abruptly (e.g., by USB disconnection). It achieves this by spawning a child process running CleanUp.main() via app_process. The child process lives independently of the server and waits for the server's stdin to close (i.e., the server dies).
Settings managed by CleanUp
| Setting | Option | Restore action |
|---|---|---|
show_touches (system) | --show-touches | Reset to "0" |
stay_on_while_plugged_in (global) | --stay-awake | Restore original value |
screen_off_timeout (system) | --screen-off-timeout | Restore original value |
| Display IME policy | --display-id with IME policy | Restore via WindowManager.setDisplayImePolicy() |
| Display power | Dynamic (from setDisplayPower()) | Restore via Device.setDisplayPower() or Device.powerOffScreen() |
The display-power setting is dynamic: the main server process communicates the desired restore state to the child process by writing a byte (0 or 1) to the child's stdin pipe. The child reads this value and updates restoreDisplayPower before the server dies.
server/src/main/java/com/genymobile/scrcpy/CleanUp.java1-271
CleanUp lifecycle
Sources: server/src/main/java/com/genymobile/scrcpy/CleanUp.java33-173 server/src/main/java/com/genymobile/scrcpy/CleanUp.java188-270
The following diagram maps the server-side control subsystem components to their Java class names.
Sources: server/src/main/java/com/genymobile/scrcpy/control/Controller.java39-137 server/src/main/java/com/genymobile/scrcpy/device/Device.java32-50 server/src/main/java/com/genymobile/scrcpy/CleanUp.java23-50
Refresh this wiki
This wiki was recently refreshed. Please wait 3 days to refresh again.