package com.genymobile.scrcpy; import android.os.Build; import android.os.HandlerThread; import android.os.MessageQueue; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.util.ArrayMap; import java.io.FileDescriptor; import java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; public final class UhidManager { // Linux: include/uapi/linux/uhid.h private static final int UHID_OUTPUT = 6; private static final int UHID_CREATE2 = 11; private static final int UHID_INPUT2 = 12; // Linux: include/uapi/linux/input.h private static final short BUS_VIRTUAL = 0x06; private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) private final ArrayMap fds = new ArrayMap<>(); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; private final HandlerThread thread = new HandlerThread("UHidManager"); private final MessageQueue queue; public UhidManager(DeviceMessageSender sender) { this.sender = sender; thread.start(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { queue = thread.getLooper().getQueue(); } else { queue = null; } } public void open(int id, byte[] reportDesc) throws IOException { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { FileDescriptor old = fds.put(id, fd); if (old != null) { Ln.w("Duplicate UHID id: " + id); close(old); } byte[] req = buildUhidCreate2Req(reportDesc); Os.write(fd, req, 0, req.length); registerUhidListener(id, fd); } catch (Exception e) { close(fd); throw e; } } catch (ErrnoException e) { throw new IOException(e); } } private void registerUhidListener(int id, FileDescriptor fd) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { try { buffer.clear(); int r = Os.read(fd2, buffer); buffer.flip(); if (r > 0) { int type = buffer.getInt(); if (type == UHID_OUTPUT) { byte[] data = extractHidOutputData(buffer); if (data != null) { DeviceMessage msg = DeviceMessage.createUhidOutput(id, data); sender.send(msg); } } } } catch (ErrnoException | InterruptedIOException e) { Ln.e("Failed to read UHID output", e); return 0; } return events; }); } } private static byte[] extractHidOutputData(ByteBuffer buffer) { /* * #define UHID_DATA_MAX 4096 * struct uhid_event { * uint32_t type; * union { * // ... * struct uhid_output_req { * __u8 data[UHID_DATA_MAX]; * __u16 size; * __u8 rtype; * }; * }; * } __attribute__((__packed__)); */ if (buffer.remaining() < 4099) { Ln.w("Incomplete HID output"); return null; } int size = buffer.getShort(buffer.position() + 4096) & 0xFFFF; if (size > 4096) { Ln.w("Incorrect HID output size: " + size); return null; } byte[] data = new byte[size]; buffer.get(data); return data; } public void writeInput(int id, byte[] data) throws IOException { FileDescriptor fd = fds.get(id); if (fd == null) { Ln.w("Unknown UHID id: " + id); return; } try { byte[] req = buildUhidInput2Req(data); Os.write(fd, req, 0, req.length); } catch (ErrnoException e) { throw new IOException(e); } } private static byte[] buildUhidCreate2Req(byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; * union { * // ... * struct uhid_create2_req { * uint8_t name[128]; * uint8_t phys[64]; * uint8_t uniq[64]; * uint16_t rd_size; * uint16_t bus; * uint32_t vendor; * uint32_t product; * uint32_t version; * uint32_t country; * uint8_t rd_data[HID_MAX_DESCRIPTOR_SIZE]; * }; * }; * } __attribute__((__packed__)); */ byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII)); buf.put(empty, 0, 256 - "scrcpy".length()); buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(0); // vendor id buf.putInt(0); // product id buf.putInt(0); // version buf.putInt(0); // country; buf.put(reportDesc); return buf.array(); } private static byte[] buildUhidInput2Req(byte[] data) { /* * struct uhid_event { * uint32_t type; * union { * // ... * struct uhid_input2_req { * uint16_t size; * uint8_t data[UHID_DATA_MAX]; * }; * }; * } __attribute__((__packed__)); */ ByteBuffer buf = ByteBuffer.allocate(6 + data.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_INPUT2); buf.putShort((short) data.length); buf.put(data); return buf.array(); } public void close(int id) { FileDescriptor fd = fds.get(id); assert fd != null; close(fd); } public void closeAll() { for (FileDescriptor fd : fds.values()) { close(fd); } } private static void close(FileDescriptor fd) { try { Os.close(fd); } catch (ErrnoException e) { Ln.e("Failed to close uhid: " + e.getMessage()); } } }