package invalid.lena.scrcpy; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.LinkedBlockingDeque; // Bidirectional bridge to the scrcpy control socket. // // Outbound: Controller hands us pre-encoded ControlMessage byte arrays // via send(); the writer thread drains them onto the OutputStream. // Queue is bounded; on overflow, the *oldest* intermediate // INJECT_TOUCH_EVENT with action=MOVE is dropped. Touch-down/up and key // events are never dropped, otherwise pointers get orphaned on the target. // // Inbound: the reader thread parses DeviceMessage frames from the // InputStream. v1 only cares about TYPE_CLIPBOARD; ACK_CLIPBOARD is // logged, UHID_OUTPUT is consumed and ignored. // // Constructor takes plain streams so this class is android-free. public final class ControlStream { public interface InboundSink { void onRemoteClipboard(String text); } private static final int TYPE_INJECT_TOUCH_EVENT = 2; private static final int ACTION_MOVE = 2; private static final int DEV_TYPE_CLIPBOARD = 0; private static final int DEV_TYPE_ACK_CLIPBOARD = 1; private static final int DEV_TYPE_UHID_OUTPUT = 2; private static final int MAX_QUEUED = 256; private final InputStream in; private final OutputStream out; private final LinkedBlockingDeque outbox = new LinkedBlockingDeque<>(MAX_QUEUED); private volatile InboundSink sink; private Thread writer; private Thread reader; private volatile boolean stop; public ControlStream(InputStream in, OutputStream out) { this.in = in; this.out = out; } // Wired after construction: the Controller that consumes inbound // messages needs this stream's send() to exist first. public void setInboundSink(InboundSink sink) { this.sink = sink; } public void start() { writer = new Thread(this::runWriter, "control-writer"); reader = new Thread(this::runReader, "control-reader"); writer.start(); reader.start(); } public void stop() { stop = true; outbox.clear(); if (writer != null) writer.interrupt(); if (reader != null) reader.interrupt(); } public void send(byte[] msg) { if (stop) return; if (outbox.offerLast(msg)) return; // Full: try to drop an intermediate touch-MOVE to make room. // Touch-down/up and non-touch messages stay. for (byte[] b : outbox) { if (b.length >= 2 && b[0] == TYPE_INJECT_TOUCH_EVENT && (b[1] & 0xff) == ACTION_MOVE) { if (outbox.remove(b)) break; } } if (!outbox.offerLast(msg)) { Log.w("control: outbox full, dropping msg type=%d", msg.length > 0 ? msg[0] & 0xff : -1); } } // ---- writer ---- public void runWriter() { try { while (!stop) { byte[] msg = outbox.takeFirst(); out.write(msg); out.flush(); } } catch (InterruptedException ignored) { } catch (IOException e) { if (!stop) Log.e(e, "control writer"); } finally { Log.i("control writer: end"); } } // ---- reader ---- public void runReader() { try { byte[] tmp = new byte[12]; while (!stop) { Wire.readFully(in, tmp, 0, 1); int type = tmp[0] & 0xff; switch (type) { case DEV_TYPE_CLIPBOARD: { Wire.readFully(in, tmp, 0, 4); int len = Wire.readBe32(tmp, 0); if (len < 0 || len > 1 << 20) { throw new IOException("clipboard len out of range: " + len); } byte[] data = new byte[len]; Wire.readFully(in, data); String text = new String(data, StandardCharsets.UTF_8); InboundSink s = sink; if (s != null) s.onRemoteClipboard(text); break; } case DEV_TYPE_ACK_CLIPBOARD: { Wire.readFully(in, tmp, 0, 8); long seq = (long) Wire.readBe32(tmp, 0) << 32 | (Wire.readBe32(tmp, 4) & 0xffffffffL); Log.i("control: ack clipboard seq=%d", seq); break; } case DEV_TYPE_UHID_OUTPUT: { Wire.readFully(in, tmp, 0, 4); int len = ((tmp[2] & 0xff) << 8) | (tmp[3] & 0xff); if (len > 0) { byte[] skip = new byte[len]; Wire.readFully(in, skip); } break; } default: throw new IOException("unknown DeviceMessage type=" + type); } } } catch (IOException e) { if (!stop) Log.e(e, "control reader"); } catch (Exception e) { Log.e(e, "control reader unexpected"); } finally { Log.i("control reader: end"); } } }