diff --git a/app/src/cli.c b/app/src/cli.c index d22096cafa..3ccd6ff14b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -112,6 +112,9 @@ scrcpy_print_usage(const char *arg0) { " Do not display device (only when screen recording is\n" " enabled).\n" "\n" + " --no-game-controller\n" + " Disable game controller support.\n" + "\n" " --no-key-repeat\n" " Do not forward repeated key events when a key is held down.\n" "\n" @@ -719,6 +722,7 @@ guess_record_format(const char *filename) { #define OPT_V4L2_SINK 1027 #define OPT_DISPLAY_BUFFER 1028 #define OPT_V4L2_BUFFER 1029 +#define OPT_NO_GAME_CONTROLLER 1030 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -745,6 +749,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"max-size", required_argument, NULL, 'm'}, {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, + {"no-game-controller", no_argument, NULL, OPT_NO_GAME_CONTROLLER}, {"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT}, {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, {"port", required_argument, NULL, 'p'}, @@ -920,6 +925,9 @@ scrcpy_parse_args(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 1257010ec7..71e59edccd 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -134,6 +134,20 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; + case CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_AXIS: + buffer_write16be(&buf[1], msg->inject_game_controller_axis.id); + buf[3] = msg->inject_game_controller_axis.axis; + buffer_write16be(&buf[4], msg->inject_game_controller_axis.value); + return 6; + case CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_BUTTON: + buffer_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 CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_DEVICE: + buffer_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 920a493ae3..5c32687b49 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -33,6 +33,9 @@ enum control_msg_type { CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, CONTROL_MSG_TYPE_ROTATE_DEVICE, + CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_AXIS, + CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_BUTTON, + CONTROL_MSG_TYPE_INJECT_GAME_CONTROLLER_DEVICE, }; enum screen_power_mode { @@ -76,6 +79,20 @@ struct control_msg { struct { enum screen_power_mode mode; } set_screen_power_mode; + 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 a5d0ad07a0..fd0ad4984f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -57,9 +57,11 @@ input_manager_init(struct input_manager *im, struct controller *controller, const struct scrcpy_options *options) { im->controller = controller; im->screen = screen; + memset(im->game_controllers, 0, sizeof(im->game_controllers)); im->repeat = 0; im->control = options->control; + im->forward_game_controllers = options->forward_game_controllers; im->forward_key_repeat = options->forward_key_repeat; im->prefer_text = options->prefer_text; im->forward_all_clicks = options->forward_all_clicks; @@ -808,6 +810,111 @@ input_manager_process_mouse_wheel(struct input_manager *im, } } +void +input_manager_process_controller_axis(struct input_manager *im, + const SDL_ControllerAxisEvent *event) { + struct control_msg msg; + msg.type = 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; + controller_push_msg(im->controller, &msg); +} + +void +input_manager_process_controller_button(struct input_manager *im, + const SDL_ControllerButtonEvent *event) { + struct control_msg msg; + msg.type = 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; + controller_push_msg(im->controller, &msg); +} + +static SDL_GameController ** +find_free_game_controller_slot(struct 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 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; +} + +void +input_manager_process_controller_device(struct 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 control_msg msg; + msg.type = 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; + controller_push_msg(im->controller, &msg); +} + bool input_manager_handle_event(struct input_manager *im, SDL_Event *event) { switch (event->type) { @@ -846,6 +953,27 @@ input_manager_handle_event(struct input_manager *im, SDL_Event *event) { case SDL_FINGERUP: input_manager_process_touch(im, &event->tfinger); return true; + case SDL_CONTROLLERAXISMOTION: + if (!im->control || !im->forward_game_controllers) { + break; + } + input_manager_process_controller_axis(im, &event->caxis); + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (!im->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 (!im->control || !im->forward_game_controllers) { + break; + } + input_manager_process_controller_device(im, &event->cdevice); + break; } return false; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 1dd7825f51..b2917a1296 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -12,15 +12,19 @@ #include "scrcpy.h" #include "screen.h" +#define MAX_GAME_CONTROLLERS 16 + struct input_manager { struct controller *controller; struct screen *screen; + SDL_GameController *game_controllers[MAX_GAME_CONTROLLERS]; // SDL reports repeated events as a boolean, but Android expects the actual // number of repetitions. This variable keeps track of the count. unsigned repeat; bool control; + bool forward_game_controllers; bool forward_key_repeat; bool prefer_text; bool forward_all_clicks; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6a2857884c..d68157fd80 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -59,7 +59,7 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { static bool sdl_init_and_configure(bool display, const char *render_driver, bool disable_screensaver) { - uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; + uint32_t flags = (display ? SDL_INIT_VIDEO : 0) | SDL_INIT_GAMECONTROLLER; if (SDL_Init(flags)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8b76fb25a2..cd65102e63 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -94,6 +94,7 @@ 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; @@ -144,6 +145,7 @@ struct scrcpy_options { .stay_awake = false, \ .force_adb_forward = false, \ .disable_screensaver = false, \ + .forward_game_controllers = true, \ .forward_key_repeat = true, \ .forward_all_clicks = false, \ .legacy_paste = false, \ diff --git a/app/src/server.c b/app/src/server.c index e3c8c34408..3e4e49f5ba 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -17,7 +17,8 @@ #define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME -#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define DEVICE_SERVER_DIR "/data/local/tmp" +#define DEVICE_SERVER_PATH DEVICE_SERVER_DIR "/scrcpy-server.jar" static char * get_server_path(void) { @@ -281,6 +282,7 @@ execute_server(struct server *server, const struct server_params *params) { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, "app_process", + "-Djna.boot.library.path=/data/local/tmp", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" # ifdef SERVER_DEBUGGER_METHOD_NEW diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index cae3eee9e0..95e20541c2 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -183,8 +183,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { goto error_mutex_destroy; } - // FIXME - const AVOutputFormat *format = find_muxer("video4linux2,v4l2"); + const AVOutputFormat *format = find_muxer("v4l2"); + if (!format) { + // Alternative name + format = find_muxer("video4linux2"); + } if (!format) { LOGE("Could not find v4l2 muxer"); goto error_cond_destroy; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index ef9247ca23..cfbefa5fe1 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -2,6 +2,8 @@ #include #include +#include +#include #include "control_msg.h" @@ -278,6 +280,73 @@ static void test_serialize_rotate_device(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_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_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; @@ -295,5 +364,8 @@ int main(int argc, char *argv[]) { test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); + 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 7cd7dbd758..167aeceb93 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -20,6 +20,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'net.java.dev.jna:jna:5.6.0@aar' testImplementation 'junit:junit:4.13' } 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 ec61a1c04d..fce73c3776 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -17,7 +17,7 @@ */ public final class CleanUp { - public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + public static final String SERVER_PATH = Server.SERVER_DIR + "/scrcpy-server.jar"; // A simple struct to be passed from the main process to the cleanup process public static class Config implements Parcelable { @@ -150,8 +150,19 @@ private 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(); try { // Wait for the server to die diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index f8edd53c11..0c87d23a4a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -17,6 +17,9 @@ public final class ControlMessage { public static final int TYPE_SET_CLIPBOARD = 9; public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_ROTATE_DEVICE = 11; + public static final int TYPE_INJECT_GAME_CONTROLLER_AXIS = 12; + public static final int TYPE_INJECT_GAME_CONTROLLER_BUTTON = 13; + public static final int TYPE_INJECT_GAME_CONTROLLER_DEVICE = 14; private int type; private String text; @@ -31,6 +34,12 @@ public final class ControlMessage { private int vScroll; private boolean paste; private int repeat; + private int gameControllerId; + private int gameControllerAxis; + private int gameControllerAxisValue; + private int gameControllerButton; + private int gameControllerButtonState; + private int gameControllerDeviceEvent; private ControlMessage() { } @@ -97,6 +106,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; @@ -154,4 +189,29 @@ public boolean getPaste() { public int getRepeat() { return repeat; } + + 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 e4ab840201..4319ba8e9f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -14,6 +14,9 @@ public class ControlMessageReader { static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; + 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 @@ -83,6 +86,15 @@ public ControlMessage next() { case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; + 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); msg = null; @@ -182,6 +194,35 @@ private ControlMessage parseSetScreenPowerMode() { return ControlMessage.createSetScreenPowerMode(mode); } + 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(); int y = 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 45882bb9c9..c7e0589f33 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -2,6 +2,7 @@ import android.os.Build; import android.os.SystemClock; +import android.util.SparseArray; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -31,11 +32,22 @@ public class Controller { private boolean keepPowerModeOff; + private SparseArray gameControllers = new SparseArray(); + private boolean gameControllersEnabled; + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; initPointers(); sender = new DeviceMessageSender(connection); + + try { + UinputDevice.loadNativeLibraries(); + gameControllersEnabled = true; + } catch (UnsatisfiedLinkError e) { + Ln.e("Could not load native libraries. Game controllers will be disabled.", e); + gameControllersEnabled = false; + } } private void initPointers() { @@ -135,6 +147,69 @@ private void handleEvent() throws IOException { case ControlMessage.TYPE_ROTATE_DEVICE: Device.rotateDevice(); 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..9000b879d7 --- /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 "Microsoft X-Box 360 pad"; + } + + 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 fdd9db8877..6c50185648 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -7,13 +7,20 @@ import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; +import android.text.TextUtils; +import java.io.InputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.util.List; import java.util.Locale; public final class Server { + public static final String SERVER_DIR = "/data/local/tmp"; + public static final String[] NATIVE_LIBRARIES = { + "libjnidispatch.so", + }; private Server() { // not instantiable @@ -21,9 +28,32 @@ private Server() { private static void scrcpy(Options options) throws IOException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); + Ln.i("Supported ABIs: " + TextUtils.join(", ", Build.SUPPORTED_ABIS)); final Device device = new Device(options); List codecOptions = CodecOption.parse(options.getCodecOptions()); + 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); + } + } + } + boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; if (options.getShowTouches() || options.getStayAwake()) { 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 da56848676..678f15f796 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -314,6 +314,76 @@ public void testParseRotateDevice() throws IOException { Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); } + @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 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 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 { ControlMessageReader reader = new ControlMessageReader();