1 package com.genymobile.scrcpy; 2 3 import java.io.EOFException; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.nio.ByteBuffer; 7 import java.nio.charset.StandardCharsets; 8 9 public class ControlMessageReader { 10 11 private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; 12 private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; 13 private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; 14 private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; 15 16 public static final int TEXT_MAX_LENGTH = 300; 17 public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; 18 private static final int RAW_BUFFER_SIZE = 1024; 19 20 private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; 21 private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); 22 private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; 23 ControlMessageReader()24 public ControlMessageReader() { 25 // invariant: the buffer is always in "get" mode 26 buffer.limit(0); 27 } 28 isFull()29 public boolean isFull() { 30 return buffer.remaining() == rawBuffer.length; 31 } 32 readFrom(InputStream input)33 public void readFrom(InputStream input) throws IOException { 34 if (isFull()) { 35 throw new IllegalStateException("Buffer full, call next() to consume"); 36 } 37 buffer.compact(); 38 int head = buffer.position(); 39 int r = input.read(rawBuffer, head, rawBuffer.length - head); 40 if (r == -1) { 41 throw new EOFException("Controller socket closed"); 42 } 43 buffer.position(head + r); 44 buffer.flip(); 45 } 46 next()47 public ControlMessage next() { 48 if (!buffer.hasRemaining()) { 49 return null; 50 } 51 int savedPosition = buffer.position(); 52 53 int type = buffer.get(); 54 ControlMessage msg; 55 switch (type) { 56 case ControlMessage.TYPE_INJECT_KEYCODE: 57 msg = parseInjectKeycode(); 58 break; 59 case ControlMessage.TYPE_INJECT_TEXT: 60 msg = parseInjectText(); 61 break; 62 case ControlMessage.TYPE_INJECT_MOUSE_EVENT: 63 msg = parseInjectMouseEvent(); 64 break; 65 case ControlMessage.TYPE_INJECT_SCROLL_EVENT: 66 msg = parseInjectScrollEvent(); 67 break; 68 case ControlMessage.TYPE_SET_CLIPBOARD: 69 msg = parseSetClipboard(); 70 break; 71 case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: 72 msg = parseSetScreenPowerMode(); 73 break; 74 case ControlMessage.TYPE_BACK_OR_SCREEN_ON: 75 case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: 76 case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: 77 case ControlMessage.TYPE_GET_CLIPBOARD: 78 msg = ControlMessage.createEmpty(type); 79 break; 80 default: 81 Ln.w("Unknown event type: " + type); 82 msg = null; 83 break; 84 } 85 86 if (msg == null) { 87 // failure, reset savedPosition 88 buffer.position(savedPosition); 89 } 90 return msg; 91 } 92 parseInjectKeycode()93 private ControlMessage parseInjectKeycode() { 94 if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { 95 return null; 96 } 97 int action = toUnsigned(buffer.get()); 98 int keycode = buffer.getInt(); 99 int metaState = buffer.getInt(); 100 return ControlMessage.createInjectKeycode(action, keycode, metaState); 101 } 102 parseString()103 private String parseString() { 104 if (buffer.remaining() < 2) { 105 return null; 106 } 107 int len = toUnsigned(buffer.getShort()); 108 if (buffer.remaining() < len) { 109 return null; 110 } 111 buffer.get(textBuffer, 0, len); 112 return new String(textBuffer, 0, len, StandardCharsets.UTF_8); 113 } 114 parseInjectText()115 private ControlMessage parseInjectText() { 116 String text = parseString(); 117 if (text == null) { 118 return null; 119 } 120 return ControlMessage.createInjectText(text); 121 } 122 parseInjectMouseEvent()123 private ControlMessage parseInjectMouseEvent() { 124 if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) { 125 return null; 126 } 127 int action = toUnsigned(buffer.get()); 128 int buttons = buffer.getInt(); 129 Position position = readPosition(buffer); 130 return ControlMessage.createInjectMouseEvent(action, buttons, position); 131 } 132 parseInjectScrollEvent()133 private ControlMessage parseInjectScrollEvent() { 134 if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { 135 return null; 136 } 137 Position position = readPosition(buffer); 138 int hScroll = buffer.getInt(); 139 int vScroll = buffer.getInt(); 140 return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); 141 } 142 parseSetClipboard()143 private ControlMessage parseSetClipboard() { 144 String text = parseString(); 145 if (text == null) { 146 return null; 147 } 148 return ControlMessage.createSetClipboard(text); 149 } 150 parseSetScreenPowerMode()151 private ControlMessage parseSetScreenPowerMode() { 152 if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { 153 return null; 154 } 155 int mode = buffer.get(); 156 return ControlMessage.createSetScreenPowerMode(mode); 157 } 158 readPosition(ByteBuffer buffer)159 private static Position readPosition(ByteBuffer buffer) { 160 int x = buffer.getInt(); 161 int y = buffer.getInt(); 162 int screenWidth = toUnsigned(buffer.getShort()); 163 int screenHeight = toUnsigned(buffer.getShort()); 164 return new Position(x, y, screenWidth, screenHeight); 165 } 166 167 @SuppressWarnings("checkstyle:MagicNumber") toUnsigned(short value)168 private static int toUnsigned(short value) { 169 return value & 0xffff; 170 } 171 172 @SuppressWarnings("checkstyle:MagicNumber") toUnsigned(byte value)173 private static int toUnsigned(byte value) { 174 return value & 0xff; 175 } 176 } 177