aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/ControlStream.java
blob: eea6c7d463ce04b7735c0c8ab0bfa373d02f4211 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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");
        }
    }
}