diff --git a/app/src/cli.c b/app/src/cli.c index 08a4aa3f8e..fdbd7a2d5c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -100,6 +100,7 @@ enum { OPT_NO_WINDOW, OPT_MOUSE_BIND, OPT_NO_MOUSE_HOVER, + OPT_NO_GAME_CONTROLLER }; struct sc_option { @@ -557,6 +558,11 @@ static const struct sc_option options[] = { .longopt_id = OPT_NO_DISPLAY, .longopt = "no-display", }, + { + .longopt_id = OPT_NO_GAME_CONTROLLER, + .longopt = "no-game-controller", + .text = "Disable game controller support.", + }, { .longopt_id = OPT_NO_KEY_REPEAT, .longopt = "no-key-repeat", @@ -2370,6 +2376,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_MIPMAPS: opts->mipmaps = false; break; + case OPT_NO_GAME_CONTROLLER: + opts->forward_game_controllers = false; + break; case OPT_NO_KEY_REPEAT: opts->forward_key_repeat = false; break; diff --git a/app/src/control_msg.c b/app/src/control_msg.c index b3da5fe5ae..45deb4f125 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -164,6 +164,20 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: // no additional data return 1; + case SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_AXIS: + sc_write16be(&buf[1], msg->inject_game_controller_axis.id); + buf[3] = msg->inject_game_controller_axis.axis; + sc_write16be(&buf[4], msg->inject_game_controller_axis.value); + return 6; + case SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_BUTTON: + sc_write16be(&buf[1], msg->inject_game_controller_button.id); + buf[3] = msg->inject_game_controller_button.button; + buf[4] = msg->inject_game_controller_button.state; + return 5; + case SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_DEVICE: + sc_write16be(&buf[1], msg->inject_game_controller_device.id); + buf[3] = msg->inject_game_controller_device.event; + return 4; default: LOGW("Unknown message type: %u", (unsigned) msg->type); return 0; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index cd1340efa7..449945bddd 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -41,6 +41,9 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_AXIS, + SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_BUTTON, + SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_DEVICE, }; enum sc_screen_power_mode { @@ -106,6 +109,20 @@ struct sc_control_msg { uint16_t size; uint8_t data[SC_HID_MAX_SIZE]; } uhid_input; + struct { + int16_t id; + uint8_t axis; + int16_t value; + } inject_game_controller_axis; + struct { + int16_t id; + uint8_t button; + uint8_t state; + } inject_game_controller_button; + struct { + int16_t id; + uint8_t event; + } inject_game_controller_device; }; }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 43b10d2de8..b66c4540ee 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -72,12 +72,15 @@ sc_input_manager_init(struct sc_input_manager *im, im->controller = params->controller; im->fp = params->fp; im->screen = params->screen; + memset(im->game_controllers, 0, sizeof(im->game_controllers)); im->kp = params->kp; im->mp = params->mp; im->mouse_bindings = params->mouse_bindings; im->has_secondary_click = mouse_bindings_has_secondary_click(&im->mouse_bindings); + im->forward_game_controllers = params->forward_game_controllers; + im->forward_all_clicks = params->forward_all_clicks; im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; @@ -931,9 +934,113 @@ sc_input_manager_process_file(struct sc_input_manager *im, } } +static void +input_manager_process_controller_axis(struct sc_input_manager *im, + const SDL_ControllerAxisEvent *event) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_AXIS; + msg.inject_game_controller_axis.id = event->which; + msg.inject_game_controller_axis.axis = event->axis; + msg.inject_game_controller_axis.value = event->value; + sc_controller_push_msg(im->controller, &msg); +} + +static void +input_manager_process_controller_button(struct sc_input_manager *im, + const SDL_ControllerButtonEvent *event) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_BUTTON; + msg.inject_game_controller_button.id = event->which; + msg.inject_game_controller_button.button = event->button; + msg.inject_game_controller_button.state = event->state; + sc_controller_push_msg(im->controller, &msg); +} + +static SDL_GameController ** +find_free_game_controller_slot(struct sc_input_manager *im) { + for (unsigned i = 0; i < MAX_GAME_CONTROLLERS; ++i) { + if (!im->game_controllers[i]) { + return &im->game_controllers[i]; + } + } + + return NULL; +} + +static bool +free_game_controller_slot(struct sc_input_manager *im, + SDL_GameController *game_controller) { + for (unsigned i = 0; i < MAX_GAME_CONTROLLERS; ++i) { + if (im->game_controllers[i] == game_controller) { + im->game_controllers[i] = NULL; + return true; + } + } + + return false; +} + +static void +input_manager_process_controller_device(struct sc_input_manager *im, + const SDL_ControllerDeviceEvent *event) { + SDL_JoystickID id; + + switch (event->type) { + case SDL_CONTROLLERDEVICEADDED: { + SDL_GameController **freeGc = find_free_game_controller_slot(im); + + if (!freeGc) { + LOGW("Controller limit reached."); + return; + } + + SDL_GameController *game_controller; + game_controller = SDL_GameControllerOpen(event->which); + + if (game_controller) { + *freeGc = game_controller; + + SDL_Joystick *joystick; + joystick = SDL_GameControllerGetJoystick(game_controller); + + id = SDL_JoystickInstanceID(joystick); + } else { + LOGW("Could not open game controller #%d", event->which); + return; + } + break; + } + + case SDL_CONTROLLERDEVICEREMOVED: { + id = event->which; + + SDL_GameController *game_controller; + game_controller = SDL_GameControllerFromInstanceID(id); + + SDL_GameControllerClose(game_controller); + + if (!free_game_controller_slot(im, game_controller)) { + LOGW("Could not find removed game controller."); + return; + } + + break; + } + + default: + return; + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_DEVICE; + msg.inject_game_controller_device.id = id; + msg.inject_game_controller_device.event = event->type; + msg.inject_game_controller_device.event -= SDL_CONTROLLERDEVICEADDED; + sc_controller_push_msg(im->controller, &msg); +} + void -sc_input_manager_handle_event(struct sc_input_manager *im, - const SDL_Event *event) { +sc_input_manager_handle_event(struct sc_input_manager *im, const SDL_Event *event) { bool control = im->controller; bool paused = im->screen->paused; switch (event->type) { @@ -980,6 +1087,28 @@ sc_input_manager_handle_event(struct sc_input_manager *im, break; } sc_input_manager_process_file(im, &event->drop); + break; } + case SDL_CONTROLLERAXISMOTION: + if (!control || !im->forward_game_controllers) { + break; + } + input_manager_process_controller_axis(im, &event->caxis); + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (!control || !im->forward_game_controllers) { + break; + } + input_manager_process_controller_button(im, &event->cbutton); + break; + case SDL_CONTROLLERDEVICEADDED: + // case SDL_CONTROLLERDEVICEREMAPPED: + case SDL_CONTROLLERDEVICEREMOVED: + if (!control || !im->forward_game_controllers) { + break; + } + input_manager_process_controller_device(im, &event->cdevice); + break; } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 03c42fe676..56c4b22ecc 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,16 +14,21 @@ #include "trait/key_processor.h" #include "trait/mouse_processor.h" +#define MAX_GAME_CONTROLLERS 16 + struct sc_input_manager { struct sc_controller *controller; struct sc_file_pusher *fp; struct sc_screen *screen; + SDL_GameController *game_controllers[MAX_GAME_CONTROLLERS]; struct sc_key_processor *kp; struct sc_mouse_processor *mp; struct sc_mouse_bindings mouse_bindings; bool has_secondary_click; + bool forward_game_controllers; + bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; @@ -51,6 +56,8 @@ struct sc_input_manager_params { struct sc_mouse_processor *mp; struct sc_mouse_bindings mouse_bindings; + bool forward_game_controllers; + bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values diff --git a/app/src/options.c b/app/src/options.c index 5556d1f987..db8a09968e 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -73,6 +73,7 @@ const struct scrcpy_options scrcpy_options_default = { .stay_awake = false, .force_adb_forward = false, .disable_screensaver = false, + .forward_game_controllers = true, .forward_key_repeat = true, .legacy_paste = false, .power_off_on_close = false, diff --git a/app/src/options.h b/app/src/options.h index f840a989d7..49508782cb 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -267,7 +267,9 @@ struct scrcpy_options { bool stay_awake; bool force_adb_forward; bool disable_screensaver; + bool forward_game_controllers; bool forward_key_repeat; + bool forward_all_clicks; bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5e78dbf37e..1251ea7aa4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -484,6 +484,13 @@ scrcpy(struct scrcpy_options *options) { } LOGD("Server connected"); + // Initialize GAMECONTROLLER subsystem after server connected + // Otherwise the initial CONTROLLERDEVICEADDED event might + // be handled by `await_for_server` + if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL: %s", SDL_GetError()); + return false; + } // It is necessarily initialized here, since the device is connected struct sc_server_info *info = &s->server.info; @@ -714,6 +721,8 @@ scrcpy(struct scrcpy_options *options) { .kp = kp, .mp = mp, .mouse_bindings = options->mouse_bindings, + .forward_game_controllers = options->forward_game_controllers, + .forward_all_clicks = options->forward_all_clicks, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, .shortcut_mods = options->shortcut_mods, diff --git a/app/src/screen.c b/app/src/screen.c index 55a06ab36d..43ffa15fee 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -482,6 +482,8 @@ sc_screen_init(struct sc_screen *screen, .kp = params->kp, .mp = params->mp, .mouse_bindings = params->mouse_bindings, + .forward_game_controllers = params->forward_game_controllers, + .forward_all_clicks = params->forward_all_clicks, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, .shortcut_mods = params->shortcut_mods, diff --git a/app/src/screen.h b/app/src/screen.h index 079d4fbb34..5d0cb0b85f 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -80,6 +80,8 @@ struct sc_screen_params { struct sc_mouse_processor *mp; struct sc_mouse_bindings mouse_bindings; + bool forward_game_controllers; + bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values diff --git a/app/src/server.c b/app/src/server.c index 4d55e99476..ef5df4a253 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -217,6 +217,7 @@ execute_server(struct sc_server *server, cmd[count++] = "shell"; cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; cmd[count++] = "app_process"; + cmd[count++] = "-Djna.boot.library.path=/data/local/tmp"; #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 7a978f2b6b..47d520e0dd 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "control_msg.h" @@ -347,6 +349,29 @@ static void test_serialize_uhid_create(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_inject_game_controller_axis(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_AXIS, + .inject_game_controller_axis = { + .id = 0x1234, + .axis = SDL_CONTROLLER_AXIS_RIGHTY, + .value = -32768, + }, + }; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 6); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_AXIS, + 0x12, 0x34, // id = 0x1234 + 0x03, // axis = SDL_CONTROLLER_AXIS_RIGHTY + 0x80, 0x00, // value = -32768 + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_uhid_input(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_UHID_INPUT, @@ -370,6 +395,29 @@ static void test_serialize_uhid_input(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_inject_game_controller_button(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_BUTTON, + .inject_game_controller_button = { + .id = 0x1234, + .button = SDL_CONTROLLER_BUTTON_START, + .state = 1, + }, + }; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 5); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_BUTTON, + 0x12, 0x34, // id = 0x1234 + 0x06, // button = SDL_CONTROLLER_BUTTON_START + 0x01, // state = 1 + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_open_hard_keyboard(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, @@ -385,6 +433,27 @@ static void test_serialize_open_hard_keyboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_inject_game_controller_device(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_DEVICE, + .inject_game_controller_device = { + .id = 0x1234, + .event = (SDL_CONTROLLERDEVICEREMOVED) - SDL_CONTROLLERDEVICEADDED, + }, + }; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 4); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_DEVICE, + 0x12, 0x34, // id = 0x1234 + 0x01, // event = GameController.DEVICE_REMOVED + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -406,5 +475,8 @@ int main(int argc, char *argv[]) { test_serialize_uhid_create(); test_serialize_uhid_input(); test_serialize_open_hard_keyboard(); + test_serialize_inject_game_controller_axis(); + test_serialize_inject_game_controller_button(); + test_serialize_inject_game_controller_device(); return 0; } diff --git a/server/build.gradle b/server/build.gradle index d17ffcb203..355ffb74c7 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -24,6 +24,7 @@ android { } dependencies { + implementation 'net.java.dev.jna:jna:5.6.0@aar' testImplementation 'junit:junit:4.13.2' } diff --git a/server/proguard-rules.pro b/server/proguard-rules.pro index f1b424510d..895e8200a7 100644 --- a/server/proguard-rules.pro +++ b/server/proguard-rules.pro @@ -19,3 +19,7 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-dontwarn java.awt.* +-keep class com.sun.jna.* { *; } +-keepclassmembers class * extends com.sun.jna.* { public *; } diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f9b1efd6e1..29a9e440e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -16,6 +16,7 @@ public final class CleanUp { private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; + public static final String SERVER_PATH = Server.SERVER_DIR + "/scrcpy-server.jar"; private static final int MSG_PARAM_SHIFT = 2; @@ -74,8 +75,19 @@ public static void unlinkSelf() { } } + private static void unlinkNativeLibs() { + for (String lib : Server.NATIVE_LIBRARIES) { + try { + new File(Server.SERVER_DIR + "/" + lib).delete(); + } catch (Exception e) { + Ln.e("Could not unlink native library " + lib, e); + } + } + } + public static void main(String... args) { unlinkSelf(); + unlinkNativeLibs(); int displayId = Integer.parseInt(args[0]); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index bcbacb4bbf..e95e187430 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -20,6 +20,9 @@ public final class ControlMessage { public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14; + public static final int TYPE_INJECT_GAME_CONTROLLER_AXIS = 15; + public static final int TYPE_INJECT_GAME_CONTROLLER_BUTTON = 16; + public static final int TYPE_INJECT_GAME_CONTROLLER_DEVICE = 17; public static final long SEQUENCE_INVALID = 0; @@ -45,6 +48,12 @@ public final class ControlMessage { private long sequence; private int id; private byte[] data; + private int gameControllerId; + private int gameControllerAxis; + private int gameControllerAxisValue; + private int gameControllerButton; + private int gameControllerButtonState; + private int gameControllerDeviceEvent; private ControlMessage() { } @@ -122,6 +131,32 @@ public static ControlMessage createSetScreenPowerMode(int mode) { return msg; } + public static ControlMessage createInjectGameControllerAxis(int id, int axis, int value) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_GAME_CONTROLLER_AXIS; + msg.gameControllerId = id; + msg.gameControllerAxis = axis; + msg.gameControllerAxisValue = value; + return msg; + } + + public static ControlMessage createInjectGameControllerButton(int id, int button, int state) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_GAME_CONTROLLER_BUTTON; + msg.gameControllerId = id; + msg.gameControllerButton = button; + msg.gameControllerButtonState = state; + return msg; + } + + public static ControlMessage createInjectGameControllerDevice(int id, int event) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_GAME_CONTROLLER_DEVICE; + msg.gameControllerId = id; + msg.gameControllerDeviceEvent = event; + return msg; + } + public static ControlMessage createEmpty(int type) { ControlMessage msg = new ControlMessage(); msg.type = type; @@ -215,4 +250,28 @@ public int getId() { public byte[] getData() { return data; } + public int getGameControllerId() { + return gameControllerId; + } + + public int getGameControllerAxis() { + return gameControllerAxis; + } + + public int getGameControllerAxisValue() { + return gameControllerAxisValue; + } + + public int getGameControllerButton() { + return gameControllerButton; + } + + public int getGameControllerButtonState() { + return gameControllerButtonState; + } + + public int getGameControllerDeviceEvent() { + return gameControllerDeviceEvent; + } + } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 1761d22880..857dff6b3e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -17,6 +17,9 @@ public class ControlMessageReader { static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 4; static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4; + static final int INJECT_GAME_CONTROLLER_AXIS_PAYLOAD_LENGTH = 5; + static final int INJECT_GAME_CONTROLLER_BUTTON_PAYLOAD_LENGTH = 4; + static final int INJECT_GAME_CONTROLLER_DEVICE_PAYLOAD_LENGTH = 3; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k @@ -94,6 +97,14 @@ public ControlMessage next() { break; case ControlMessage.TYPE_UHID_INPUT: msg = parseUhidInput(); + case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_AXIS: + msg = parseInjectGameControllerAxis(); + break; + case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_BUTTON: + msg = parseInjectGameControllerButton(); + break; + case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_DEVICE: + msg = parseInjectGameControllerDevice(); break; default: Ln.w("Unknown event type: " + type); @@ -244,6 +255,35 @@ private ControlMessage parseUhidInput() { } return ControlMessage.createUhidInput(id, data); } + + private ControlMessage parseInjectGameControllerAxis() { + if (buffer.remaining() < INJECT_GAME_CONTROLLER_AXIS_PAYLOAD_LENGTH) { + return null; + } + int id = buffer.getShort(); + int axis = buffer.get(); + int value = buffer.getShort(); + return ControlMessage.createInjectGameControllerAxis(id, axis, value); + } + + private ControlMessage parseInjectGameControllerButton() { + if (buffer.remaining() < INJECT_GAME_CONTROLLER_BUTTON_PAYLOAD_LENGTH) { + return null; + } + int id = buffer.getShort(); + int button = buffer.get(); + int state = buffer.get(); + return ControlMessage.createInjectGameControllerButton(id, button, state); + } + + private ControlMessage parseInjectGameControllerDevice() { + if (buffer.remaining() < INJECT_GAME_CONTROLLER_DEVICE_PAYLOAD_LENGTH) { + return null; + } + int id = buffer.getShort(); + int event = buffer.get(); + return ControlMessage.createInjectGameControllerDevice(id, event); + } private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 87faf8ba29..c653568435 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.os.Build; import android.os.SystemClock; +import android.util.SparseArray; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -46,6 +47,9 @@ public class Controller implements AsyncProcessor { private boolean keepPowerModeOff; + private SparseArray gameControllers = new SparseArray(); + private boolean gameControllersEnabled; + public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.device = device; this.controlChannel = controlChannel; @@ -54,6 +58,14 @@ public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, this.powerOn = powerOn; initPointers(); sender = new DeviceMessageSender(controlChannel); + + try { + UinputDevice.loadNativeLibraries(); + gameControllersEnabled = true; + } catch (UnsatisfiedLinkError e) { + Ln.e("Could not load native libraries. Game controllers will be disabled.", e); + gameControllersEnabled = false; + } } private UhidManager getUhidManager() { @@ -213,6 +225,69 @@ private boolean handleEvent() throws IOException { case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; + case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_AXIS: + if (gameControllersEnabled) { + int id = msg.getGameControllerId(); + int axis = msg.getGameControllerAxis(); + int value = msg.getGameControllerAxisValue(); + + GameController controller = gameControllers.get(id); + + if (controller != null) { + controller.setAxis(axis, value); + } else { + Ln.w("Received data for non-existant controller."); + } + break; + } + break; + case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_BUTTON: + if (gameControllersEnabled) { + int id = msg.getGameControllerId(); + int button = msg.getGameControllerButton(); + int state = msg.getGameControllerButtonState(); + + GameController controller = gameControllers.get(id); + + if (controller != null) { + controller.setButton(button, state); + } else { + Ln.w("Received data for non-existant controller."); + } + } + break; + case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_DEVICE: + if (gameControllersEnabled) { + int id = msg.getGameControllerId(); + int event = msg.getGameControllerDeviceEvent(); + + switch (event) { + case GameController.DEVICE_ADDED: + try { + gameControllers.append(id, new GameController()); + } catch (Exception e) { + Ln.e("Failed to add new game controller. Game controllers will be disabled.", e); + gameControllersEnabled = false; + } + break; + + case GameController.DEVICE_REMOVED: + GameController controller = gameControllers.get(id); + + if (controller != null) { + controller.close(); + gameControllers.delete(id); + } else { + Ln.w("Non-existant game controller removed."); + } + + break; + + default: + Ln.w("Unknown game controller event received."); + } + } + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/GameController.java b/server/src/main/java/com/genymobile/scrcpy/GameController.java new file mode 100644 index 0000000000..fb81ea4694 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/GameController.java @@ -0,0 +1,194 @@ +package com.genymobile.scrcpy; + +public final class GameController extends UinputDevice { + private static final short XBOX_BTN_A = BTN_A; + private static final short XBOX_BTN_B = BTN_B; + private static final short XBOX_BTN_X = BTN_X; + private static final short XBOX_BTN_Y = BTN_Y; + private static final short XBOX_BTN_BACK = BTN_SELECT; + private static final short XBOX_BTN_START = BTN_START; + private static final short XBOX_BTN_LB = BTN_TL; + private static final short XBOX_BTN_RB = BTN_TR; + private static final short XBOX_BTN_GUIDE = BTN_MODE; + private static final short XBOX_BTN_LS = BTN_THUMBL; + private static final short XBOX_BTN_RS = BTN_THUMBR; + + private static final short XBOX_ABS_LSX = ABS_X; + private static final short XBOX_ABS_LSY = ABS_Y; + private static final short XBOX_ABS_RSX = ABS_RX; + private static final short XBOX_ABS_RSY = ABS_RY; + private static final short XBOX_ABS_DPADX = ABS_HAT0X; + private static final short XBOX_ABS_DPADY = ABS_HAT0Y; + private static final short XBOX_ABS_LT = ABS_Z; + private static final short XBOX_ABS_RT = ABS_RZ; + + public static final int SDL_CONTROLLER_AXIS_LEFTX = 0; + public static final int SDL_CONTROLLER_AXIS_LEFTY = 1; + public static final int SDL_CONTROLLER_AXIS_RIGHTX = 2; + public static final int SDL_CONTROLLER_AXIS_RIGHTY = 3; + public static final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4; + public static final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5; + + public static final int SDL_CONTROLLER_BUTTON_A = 0; + public static final int SDL_CONTROLLER_BUTTON_B = 1; + public static final int SDL_CONTROLLER_BUTTON_X = 2; + public static final int SDL_CONTROLLER_BUTTON_Y = 3; + public static final int SDL_CONTROLLER_BUTTON_BACK = 4; + public static final int SDL_CONTROLLER_BUTTON_GUIDE = 5; + public static final int SDL_CONTROLLER_BUTTON_START = 6; + public static final int SDL_CONTROLLER_BUTTON_LEFTSTICK = 7; + public static final int SDL_CONTROLLER_BUTTON_RIGHTSTICK = 8; + public static final int SDL_CONTROLLER_BUTTON_LEFTSHOULDER = 9; + public static final int SDL_CONTROLLER_BUTTON_RIGHTSHOULDER = 10; + public static final int SDL_CONTROLLER_BUTTON_DPAD_UP = 11; + public static final int SDL_CONTROLLER_BUTTON_DPAD_DOWN = 12; + public static final int SDL_CONTROLLER_BUTTON_DPAD_LEFT = 13; + public static final int SDL_CONTROLLER_BUTTON_DPAD_RIGHT = 14; + + public GameController() { + setup(); + } + + protected void setupKeys() { + addKey(XBOX_BTN_A); + addKey(XBOX_BTN_B); + addKey(XBOX_BTN_X); + addKey(XBOX_BTN_Y); + addKey(XBOX_BTN_BACK); + addKey(XBOX_BTN_START); + addKey(XBOX_BTN_LB); + addKey(XBOX_BTN_RB); + addKey(XBOX_BTN_GUIDE); + addKey(XBOX_BTN_LS); + addKey(XBOX_BTN_RS); + } + + protected boolean hasKeys() { + return true; + } + + protected void setupAbs() { + addAbs(XBOX_ABS_LSX, -32768, 32767, 16, 128); + addAbs(XBOX_ABS_LSY, -32768, 32767, 16, 128); + addAbs(XBOX_ABS_RSX, -32768, 32767, 16, 128); + addAbs(XBOX_ABS_RSY, -32768, 32767, 16, 128); + addAbs(XBOX_ABS_DPADX, -1, 1, 0, 0); + addAbs(XBOX_ABS_DPADY, -1, 1, 0, 0); + // These values deviate from the real Xbox 360 controller, + // but allow higher precision (eg. Xbox One controller) + addAbs(XBOX_ABS_LT, 0, 32767, 0, 0); + addAbs(XBOX_ABS_RT, 0, 32767, 0, 0); + } + + protected boolean hasAbs() { + return true; + } + + protected short getVendor() { + return 0x045e; + } + + protected short getProduct() { + return 0x028e; + } + + protected String getName() { + return "scrcpy Xbox Controller"; + } + + private static short translateAxis(int axis) { + switch (axis) { + case SDL_CONTROLLER_AXIS_LEFTX: + return XBOX_ABS_LSX; + + case SDL_CONTROLLER_AXIS_LEFTY: + return XBOX_ABS_LSY; + + case SDL_CONTROLLER_AXIS_RIGHTX: + return XBOX_ABS_RSX; + + case SDL_CONTROLLER_AXIS_RIGHTY: + return XBOX_ABS_RSY; + + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return XBOX_ABS_LT; + + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return XBOX_ABS_RT; + + default: + return 0; + } + } + + private static short translateButton(int button) { + switch (button) { + case SDL_CONTROLLER_BUTTON_A: + return XBOX_BTN_A; + + case SDL_CONTROLLER_BUTTON_B: + return XBOX_BTN_B; + + case SDL_CONTROLLER_BUTTON_X: + return XBOX_BTN_X; + + case SDL_CONTROLLER_BUTTON_Y: + return XBOX_BTN_Y; + + case SDL_CONTROLLER_BUTTON_BACK: + return XBOX_BTN_BACK; + + case SDL_CONTROLLER_BUTTON_GUIDE: + return XBOX_BTN_GUIDE; + + case SDL_CONTROLLER_BUTTON_START: + return XBOX_BTN_START; + + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + return XBOX_BTN_LS; + + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return XBOX_BTN_RS; + + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + return XBOX_BTN_LB; + + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + return XBOX_BTN_RB; + + default: + return 0; + } + } + + public void setAxis(int axis, int value) { + emitAbs(translateAxis(axis), value); + emitReport(); + } + + public void setButton(int button, int state) { + // DPad buttons are usually reported as axes + + switch (button) { + case SDL_CONTROLLER_BUTTON_DPAD_UP: + emitAbs(XBOX_ABS_DPADY, state != 0 ? -1 : 0); + break; + + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + emitAbs(XBOX_ABS_DPADY, state != 0 ? 1 : 0); + break; + + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + emitAbs(XBOX_ABS_DPADX, state != 0 ? -1 : 0); + break; + + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + emitAbs(XBOX_ABS_DPADX, state != 0 ? 1 : 0); + break; + + default: + emitKey(translateButton(button), state); + } + emitReport(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 587a46df3e..1f0dcf6ad7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -2,8 +2,11 @@ import android.os.BatteryManager; import android.os.Build; +import android.text.TextUtils; import java.io.File; +import java.io.InputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -18,6 +21,11 @@ public final class Server { SERVER_PATH = classPaths[0]; } + public static final String SERVER_DIR = "/data/local/tmp"; + public static final String[] NATIVE_LIBRARIES = { + "libjnidispatch.so", + }; + private static class Completion { private int running; private boolean fatalError; @@ -102,6 +110,28 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc throw new ConfigurationException("Camera mirroring is not supported"); } + for (String lib : NATIVE_LIBRARIES) { + for (String abi : Build.SUPPORTED_ABIS) { + try { + InputStream resStream = Server.class.getResourceAsStream("/lib/" + abi + "/" + lib); + FileOutputStream fileStream = new FileOutputStream(SERVER_DIR + "/" + lib); + + byte[] buffer = new byte[1024]; + int length; + while ((length = resStream.read(buffer)) > 0) { + fileStream.write(buffer, 0, length); + } + + resStream.close(); + fileStream.close(); + + break; + } catch (Exception e) { + Ln.e("Could not extract native library for " + abi, e); + } + } + } + CleanUp cleanUp = null; Thread initThread = null; diff --git a/server/src/main/java/com/genymobile/scrcpy/UinputDevice.java b/server/src/main/java/com/genymobile/scrcpy/UinputDevice.java new file mode 100644 index 0000000000..ac29abdef5 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/UinputDevice.java @@ -0,0 +1,411 @@ +package com.genymobile.scrcpy; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Platform; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.ptr.IntByReference; + +import java.util.Arrays; +import java.util.List; + +public abstract class UinputDevice { + public static final int DEVICE_ADDED = 0; + public static final int DEVICE_REMOVED = 1; + + private static final int UINPUT_MAX_NAME_SIZE = 80; + private static final int ABS_MAX = 0x3f; + private static final int ABS_CNT = ABS_MAX + 1; + + @SuppressWarnings("checkstyle:VisibilityModifier") + public static class InputId extends Structure { + public short bustype = 0; + public short vendor = 0; + public short product = 0; + public short version = 0; + + @Override + protected List getFieldOrder() { + return Arrays.asList("bustype", "vendor", "product", "version"); + } + } + + @SuppressWarnings("checkstyle:VisibilityModifier") + public static class UinputSetup extends Structure { + public InputId id; + public byte[] name = new byte[UINPUT_MAX_NAME_SIZE]; + public int ffEffectsMax = 0; + + @Override + protected List getFieldOrder() { + return Arrays.asList("id", "name", "ffEffectsMax"); + } + } + + @SuppressWarnings("checkstyle:VisibilityModifier") + public static class UinputUserDev extends Structure { + public byte[] name = new byte[UINPUT_MAX_NAME_SIZE]; + public InputId id; + public int ffEffectsMax = 0; + public int[] absmax = new int[ABS_CNT]; + public int[] absmin = new int[ABS_CNT]; + public int[] absfuzz = new int[ABS_CNT]; + public int[] absflat = new int[ABS_CNT]; + + @Override + protected List getFieldOrder() { + return Arrays.asList("name", "id", "ffEffectsMax", "absmax", "absmin", "absfuzz", "absflat"); + } + }; + + @SuppressWarnings("checkstyle:VisibilityModifier") + public static class InputAbsinfo extends Structure { + public int value = 0; + public int minimum = 0; + public int maximum = 0; + public int fuzz = 0; + public int flat = 0; + public int resolution = 0; + + @Override + protected List getFieldOrder() { + return Arrays.asList("value", "minimum", "maximum", "fuzz", "flat", "resolution"); + } + }; + + @SuppressWarnings("checkstyle:VisibilityModifier") + public static class UinputAbsSetup extends Structure { + public short code; + public InputAbsinfo absinfo; + + @Override + protected List getFieldOrder() { + return Arrays.asList("code", "absinfo"); + } + }; + + @SuppressWarnings("checkstyle:VisibilityModifier") + public static class InputEvent32 extends Structure { + public long time = 0; + public short type = 0; + public short code = 0; + public int value = 0; + + @Override + protected List getFieldOrder() { + return Arrays.asList("time", "type", "code", "value"); + } + } + + @SuppressWarnings("checkstyle:VisibilityModifier") + public static class InputEvent64 extends Structure { + public long sec = 0; + public long usec = 0; + public short type = 0; + public short code = 0; + public int value = 0; + + @Override + protected List getFieldOrder() { + return Arrays.asList("sec", "usec", "type", "code", "value"); + } + } + + private static final int IOC_NONE = 0; + private static final int IOC_WRITE = 1; + private static final int IOC_READ = 2; + + private static final int IOC_DIRSHIFT = 30; + private static final int IOC_TYPESHIFT = 8; + private static final int IOC_NRSHIFT = 0; + private static final int IOC_SIZESHIFT = 16; + + private static int ioc(int dir, int type, int nr, int size) { + return (dir << IOC_DIRSHIFT) + | (type << IOC_TYPESHIFT) + | (nr << IOC_NRSHIFT) + | (size << IOC_SIZESHIFT); + } + + private static int io(int type, int nr, int size) { + return ioc(IOC_NONE, type, nr, size); + } + + private static int ior(int type, int nr, int size) { + return ioc(IOC_READ, type, nr, size); + } + + private static int iow(int type, int nr, int size) { + return ioc(IOC_WRITE, type, nr, size); + } + + private static final int O_WRONLY = 01; + private static final int O_NONBLOCK = 04000; + + private static final int BUS_USB = 0x03; + + private static final int UINPUT_IOCTL_BASE = 'U'; + + private static final int UI_GET_VERSION = ior(UINPUT_IOCTL_BASE, 45, 4); // 0.5+ + private static final int UI_SET_EVBIT = iow(UINPUT_IOCTL_BASE, 100, 4); + private static final int UI_SET_KEYBIT = iow(UINPUT_IOCTL_BASE, 101, 4); + private static final int UI_SET_ABSBIT = iow(UINPUT_IOCTL_BASE, 103, 4); + private static final int UI_ABS_SETUP = iow(UINPUT_IOCTL_BASE, 4, new UinputAbsSetup().size()); // 0.5+ + + private static final int UI_DEV_SETUP = iow(UINPUT_IOCTL_BASE, 3, new UinputSetup().size()); // 0.5+ + private static final int UI_DEV_CREATE = io(UINPUT_IOCTL_BASE, 1, 0); + private static final int UI_DEV_DESTROY = io(UINPUT_IOCTL_BASE, 2, 0); + + private static final short EV_SYN = 0x00; + private static final short EV_KEY = 0x01; + private static final short EV_ABS = 0x03; + + private static final short SYN_REPORT = 0x00; + + protected static final short BTN_A = 0x130; + protected static final short BTN_B = 0x131; + protected static final short BTN_X = 0x133; + protected static final short BTN_Y = 0x134; + protected static final short BTN_TL = 0x136; + protected static final short BTN_TR = 0x137; + protected static final short BTN_SELECT = 0x13a; + protected static final short BTN_START = 0x13b; + protected static final short BTN_MODE = 0x13c; + protected static final short BTN_THUMBL = 0x13d; + protected static final short BTN_THUMBR = 0x13e; + + protected static final short ABS_X = 0x00; + protected static final short ABS_Y = 0x01; + protected static final short ABS_Z = 0x02; + protected static final short ABS_RX = 0x03; + protected static final short ABS_RY = 0x04; + protected static final short ABS_RZ = 0x05; + protected static final short ABS_HAT0X = 0x10; + protected static final short ABS_HAT0Y = 0x11; + + private int fd = -1; + private int[] absmax; + private int[] absmin; + private int[] absfuzz; + private int[] absflat; + + public interface LibC extends Library { + int open(String pathname, int flags) throws LastErrorException; + int ioctl(int fd, long request, Object... args) throws LastErrorException; + long write(int fd, Pointer buf, long count) throws LastErrorException; + int close(int fd) throws LastErrorException; + } + + private static LibC libC; + private static int version = 0; + + /// Must be the first method called + public static void loadNativeLibraries() { + libC = (LibC) Native.load("c", LibC.class); + } + + protected void setup() { + try { + fd = libC.open("/dev/uinput", O_WRONLY | O_NONBLOCK); + } catch (LastErrorException e) { + throw new UinputUnsupportedException(e); + } + + if (version == 0) { + try { + IntByReference versionRef = new IntByReference(); + libC.ioctl(fd, UI_GET_VERSION, versionRef); + version = versionRef.getValue(); + } catch (LastErrorException e) { + version = -1; + } + + if (version >= 0) { + Ln.i(String.format("Using uinput version 0.%d", version)); + } else { + Ln.i(String.format("Using unknown uinput version. Assuming at least 0.3.")); + } + } + + if (hasKeys()) { + try { + libC.ioctl(fd, UI_SET_EVBIT, EV_KEY); + } catch (LastErrorException e) { + throw new RuntimeException("Could not enable key events.", e); + } + setupKeys(); + } + + if (hasAbs()) { + try { + libC.ioctl(fd, UI_SET_EVBIT, EV_ABS); + } catch (LastErrorException e) { + throw new RuntimeException("Could not enable absolute events.", e); + } + + if (version < 5) { + absmax = new int[ABS_CNT]; + absmin = new int[ABS_CNT]; + absfuzz = new int[ABS_CNT]; + absflat = new int[ABS_CNT]; + } + + setupAbs(); + } + + if (version >= 5) { + UinputSetup usetup = new UinputSetup(); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = getVendor(); + usetup.id.product = getProduct(); + byte[] name = getName().getBytes(); + System.arraycopy(name, 0, usetup.name, 0, name.length); + + try { + libC.ioctl(fd, UI_DEV_SETUP, usetup); + } catch (LastErrorException e) { + close(); + throw new RuntimeException("Could not setup uinput device.", e); + } + } else { + UinputUserDev userDev = new UinputUserDev(); + userDev.id.bustype = BUS_USB; + userDev.id.vendor = getVendor(); + userDev.id.product = getProduct(); + byte[] name = getName().getBytes(); + System.arraycopy(name, 0, userDev.name, 0, name.length); + + userDev.absmax = absmax; + userDev.absmin = absmin; + userDev.absfuzz = absfuzz; + userDev.absflat = absflat; + + userDev.write(); + + try { + libC.write(fd, userDev.getPointer(), userDev.size()); + } catch (LastErrorException e) { + close(); + throw new RuntimeException("Could not setup uinput device using legacy method.", e); + } + } + + try { + libC.ioctl(fd, UI_DEV_CREATE); + } catch (LastErrorException e) { + close(); + throw new RuntimeException("Could not create uinput device.", e); + } + } + + public void close() { + if (fd != -1) { + try { + libC.ioctl(fd, UI_DEV_DESTROY); + } catch (LastErrorException e) { + Ln.e("Could not destroy uinput device.", e); + } + try { + libC.close(fd); + } catch (LastErrorException e) { + Ln.e("Could not close uinput device.", e); + } + fd = -1; + } + } + + protected abstract void setupKeys(); + protected abstract boolean hasKeys(); + protected abstract void setupAbs(); + protected abstract boolean hasAbs(); + protected abstract short getVendor(); + protected abstract short getProduct(); + protected abstract String getName(); + + protected void addKey(int key) { + try { + libC.ioctl(fd, UI_SET_KEYBIT, key); + } catch (LastErrorException e) { + Ln.e("Could not add key event.", e); + } + } + + protected void addAbs(short code, int minimum, int maximum, int fuzz, int flat) { + try { + libC.ioctl(fd, UI_SET_ABSBIT, code); + } catch (LastErrorException e) { + Ln.e("Could not add absolute event.", e); + } + + if (version >= 5) { + UinputAbsSetup absSetup = new UinputAbsSetup(); + + absSetup.code = code; + absSetup.absinfo.minimum = minimum; + absSetup.absinfo.maximum = maximum; + absSetup.absinfo.fuzz = fuzz; + absSetup.absinfo.flat = flat; + + try { + libC.ioctl(fd, UI_ABS_SETUP, absSetup); + } catch (LastErrorException e) { + Ln.e("Could not set absolute event info.", e); + } + } else { + absmin[code] = minimum; + absmax[code] = maximum; + absfuzz[code] = fuzz; + absflat[code] = flat; + } + } + + private static void emit32(int fd, short type, short code, int val) { + InputEvent32 ie = new InputEvent32(); + + ie.type = type; + ie.code = code; + ie.value = val; + + ie.write(); + + libC.write(fd, ie.getPointer(), ie.size()); + } + + private static void emit64(int fd, short type, short code, int val) { + InputEvent64 ie = new InputEvent64(); + + ie.type = type; + ie.code = code; + ie.value = val; + + ie.write(); + + libC.write(fd, ie.getPointer(), ie.size()); + } + + private static void emit(int fd, short type, short code, int val) { + try { + if (Platform.is64Bit()) { + emit64(fd, type, code, val); + } else { + emit32(fd, type, code, val); + } + } catch (LastErrorException e) { + Ln.e("Could not emit event.", e); + } + } + + protected void emitAbs(short abs, int value) { + emit(fd, EV_ABS, abs, value); + } + + protected void emitKey(short key, int state) { + emit(fd, EV_KEY, key, state); + } + + protected void emitReport() { + emit(fd, EV_SYN, SYN_REPORT, 0); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/UinputUnsupportedException.java b/server/src/main/java/com/genymobile/scrcpy/UinputUnsupportedException.java new file mode 100644 index 0000000000..13c6b84991 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/UinputUnsupportedException.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy; + +public class UinputUnsupportedException extends RuntimeException { + public UinputUnsupportedException(Exception e) { + super("device does not support uinput without root", e); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 0c8086f7e7..e0376430da 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -328,6 +328,7 @@ public void testParseUhidCreate() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_CREATE); dos.writeShort(42); // id byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; @@ -343,6 +344,31 @@ public void testParseUhidCreate() throws IOException { Assert.assertEquals(42, event.getId()); Assert.assertArrayEquals(data, event.getData()); } + + @Test + public void testParseGameControllerAxisEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlMessage.TYPE_INJECT_GAME_CONTROLLER_AXIS); + dos.writeShort(0x1234); + dos.writeByte(GameController.SDL_CONTROLLER_AXIS_RIGHTY); + dos.writeShort(-32768); + byte[] packet = bos.toByteArray(); + + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_GAME_CONTROLLER_AXIS_PAYLOAD_LENGTH, packet.length - 1); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_GAME_CONTROLLER_AXIS, event.getType()); + Assert.assertEquals(0x1234, event.getGameControllerId()); + Assert.assertEquals(GameController.SDL_CONTROLLER_AXIS_RIGHTY, event.getGameControllerAxis()); + Assert.assertEquals(-32768, event.getGameControllerAxisValue()); + } @Test public void testParseUhidInput() throws IOException { @@ -350,6 +376,7 @@ public void testParseUhidInput() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_INPUT); dos.writeShort(42); // id byte[] data = {1, 2, 3, 4, 5}; @@ -366,12 +393,38 @@ public void testParseUhidInput() throws IOException { Assert.assertArrayEquals(data, event.getData()); } + @Test + public void testParseGameControllerButtonEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlMessage.TYPE_INJECT_GAME_CONTROLLER_BUTTON); + dos.writeShort(0x1234); + dos.writeByte(GameController.SDL_CONTROLLER_BUTTON_START); + dos.writeByte(1); + byte[] packet = bos.toByteArray(); + + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_GAME_CONTROLLER_BUTTON_PAYLOAD_LENGTH, packet.length - 1); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_GAME_CONTROLLER_BUTTON, event.getType()); + Assert.assertEquals(0x1234, event.getGameControllerId()); + Assert.assertEquals(GameController.SDL_CONTROLLER_BUTTON_START, event.getGameControllerButton()); + Assert.assertEquals(1, event.getGameControllerButtonState()); + } + @Test public void testParseOpenHardKeyboardSettings() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); byte[] packet = bos.toByteArray(); @@ -381,6 +434,29 @@ public void testParseOpenHardKeyboardSettings() throws IOException { Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); } + + @Test + public void testParseGameControllerDeviceEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlMessage.TYPE_INJECT_GAME_CONTROLLER_DEVICE); + dos.writeShort(0x1234); + dos.writeByte(GameController.DEVICE_REMOVED); + byte[] packet = bos.toByteArray(); + + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_GAME_CONTROLLER_DEVICE_PAYLOAD_LENGTH, packet.length - 1); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_GAME_CONTROLLER_DEVICE, event.getType()); + Assert.assertEquals(0x1234, event.getGameControllerId()); + Assert.assertEquals(GameController.DEVICE_REMOVED, event.getGameControllerDeviceEvent()); + } @Test public void testMultiEvents() throws IOException {