aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/ControlMessages.java
blob: 6455a6564fcc21303dec5e885faff3711abea226 (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
package invalid.lena.scrcpy;

import java.nio.charset.StandardCharsets;

// Pure-java encoders for scrcpy control messages. Layouts mirror
// com.genymobile.scrcpy.control.ControlMessageReader in the scrcpy
// server source. Extracted from Controller so the wire format can be
// unit-tested without android.* on the classpath.
public final class ControlMessages {

    // scrcpy caps a whole control message at MESSAGE_MAX_SIZE = 256 KiB.
    // For SET_CLIPBOARD that leaves 256 KiB minus the 14-byte header
    // (type 1, sequence 8, paste flag 1, length 4); see the server's
    // ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH. Going over makes
    // the server raise ControlProtocolException and drop the control
    // connection, taking the session with it, so refuse locally instead.
    static final int MAX_CLIPBOARD_BYTES = (1 << 18) - 14;

    // The reverse direction, from the server's DeviceMessageWriter:
    // 256 KiB minus its 5-byte header (type 1, length 4). Used to bound
    // what we are willing to read off the control socket.
    static final int MAX_DEVICE_CLIPBOARD_BYTES = (1 << 18) - 5;

    public static final int TYPE_INJECT_KEYCODE      = 0;
    public static final int TYPE_INJECT_TOUCH_EVENT  = 2;
    public static final int TYPE_BACK_OR_SCREEN_ON   = 4;
    public static final int TYPE_SET_CLIPBOARD       = 9;
    public static final int TYPE_RESET_VIDEO         = 17;

    // KeyEvent.ACTION_DOWN / ACTION_UP. Mirror the int values rather
    // than depend on android.view.KeyEvent so this stays android-free.
    public static final int ACTION_DOWN = 0;
    public static final int ACTION_UP   = 1;

    // AOSP keycodes, mirrored rather than imported so this stays
    // android-free. KeyEvent.KEYCODE_HOME / KEYCODE_APP_SWITCH.
    public static final int KEYCODE_HOME       = 3;
    public static final int KEYCODE_APP_SWITCH = 187;

    public static final int TOUCH_MSG_LEN = 32; // 1 + 1 + 8 + 4 + 4 + 2 + 2 + 2 + 4 + 4
    public static final int KEY_MSG_LEN   = 14; // 1 + 1 + 4 + 4 + 4

    private ControlMessages() {}

    public static byte[] touch(int action, long pointerId,
                               int x, int y, int targetW, int targetH,
                               int pressureU16, int actionButton, int buttons) {
        if (targetW < 1 || targetW > 0xffff || targetH < 1 || targetH > 0xffff) {
            throw new IllegalArgumentException("touch target size is out of range");
        }
        if (x < 0 || x >= targetW || y < 0 || y >= targetH) {
            throw new IllegalArgumentException("touch position is out of range");
        }
        if (pressureU16 < 0 || pressureU16 > 0xffff) {
            throw new IllegalArgumentException("touch pressure is out of range");
        }
        byte[] m = new byte[TOUCH_MSG_LEN];
        m[0] = TYPE_INJECT_TOUCH_EVENT;
        m[1] = (byte) action;
        Wire.writeBe64(m, 2,  pointerId);
        Wire.writeBe32(m, 10, x);
        Wire.writeBe32(m, 14, y);
        m[18] = (byte)(targetW >>> 8); m[19] = (byte) targetW;
        m[20] = (byte)(targetH >>> 8); m[21] = (byte) targetH;
        m[22] = (byte)(pressureU16 >>> 8); m[23] = (byte) pressureU16;
        Wire.writeBe32(m, 24, actionButton);
        Wire.writeBe32(m, 28, buttons);
        return m;
    }

    public static byte[] keycode(int action, int keycode, int repeat, int metaState) {
        byte[] m = new byte[KEY_MSG_LEN];
        m[0] = TYPE_INJECT_KEYCODE;
        m[1] = (byte) action;
        Wire.writeBe32(m, 2,  keycode);
        Wire.writeBe32(m, 6,  repeat);
        Wire.writeBe32(m, 10, metaState);
        return m;
    }

    // Single button-press event. scrcpy server interprets this as Back
    // when the screen is on, or screen-on when it's off. We send a
    // DOWN+UP pair to make a tap; this helper builds one byte pair.
    public static byte[] backOrScreenOn(int action) {
        return new byte[]{(byte) TYPE_BACK_OR_SCREEN_ON, (byte) action};
    }

    public static byte[] resetVideo() {
        return new byte[]{(byte) TYPE_RESET_VIDEO};
    }

    public static byte[] setClipboard(long sequence, boolean paste, String text) {
        // UTF-8 is at least one byte per char, so this rejects the
        // hopeless cases without encoding a huge string first. The byte
        // count below is the check that actually matters.
        if (text.length() > MAX_CLIPBOARD_BYTES) {
            throw new IllegalArgumentException("clipboard text is too large");
        }
        byte[] data = text.getBytes(StandardCharsets.UTF_8);
        if (data.length > MAX_CLIPBOARD_BYTES) {
            throw new IllegalArgumentException("clipboard UTF-8 data is too large");
        }
        byte[] m = new byte[1 + 8 + 1 + 4 + data.length];
        m[0] = TYPE_SET_CLIPBOARD;
        Wire.writeBe64(m, 1, sequence);
        m[9] = (byte)(paste ? 1 : 0);
        Wire.writeBe32(m, 10, data.length);
        System.arraycopy(data, 0, m, 14, data.length);
        return m;
    }
}