aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/ControlStream.java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/ControlStream.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/ControlStream.java154
1 files changed, 154 insertions, 0 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/ControlStream.java b/app/src/main/java/invalid/lena/scrcpy/ControlStream.java
new file mode 100644
index 0000000..eea6c7d
--- /dev/null
+++ b/app/src/main/java/invalid/lena/scrcpy/ControlStream.java
@@ -0,0 +1,154 @@
+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<byte[]> 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");
+ }
+ }
+}