diff --git a/app/src/server.c b/app/src/server.c index e3c8c344..90089325 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) { @@ -280,6 +281,7 @@ execute_server(struct server *server, const struct server_params *params) { const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, + "LD_LIBRARY_PATH=" DEVICE_SERVER_DIR, "app_process", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" diff --git a/server/build.gradle b/server/build.gradle index 7cd7dbd7..167aeceb 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 f1b42451..895e8200 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 ec61a1c0..578b19c0 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 @@ import java.io.IOException; */ 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 @@ public final class CleanUp { } } + 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/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 68b9a24c..e6946729 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 @@ package com.genymobile.scrcpy; 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 { + GameController.load_native_libraries(); + gameControllersEnabled = true; + } catch (UnsatisfiedLinkError e) { + Ln.e("Could not load native libraries. Game controllers will be disabled.", e); + gameControllersEnabled = false; + } } private void initPointers() { @@ -136,26 +148,51 @@ public class Controller { Device.rotateDevice(); break; case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_AXIS: - { + if (gameControllersEnabled) { int id = msg.getGameControllerId(); int axis = msg.getGameControllerAxis(); int value = msg.getGameControllerAxisValue(); - Ln.d(String.format("Received gc axis: %d %d %d", id, axis, value)); + if (!gameControllers.contains(id)) + { + Ln.w("Received data for non-existant controller."); + break; + } + gameControllers.get(id).setAxis(axis, value); } break; case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_BUTTON: - { + if (gameControllersEnabled) { int id = msg.getGameControllerId(); int button = msg.getGameControllerButton(); int state = msg.getGameControllerButtonState(); - Ln.d(String.format("Received gc button: %d %d %d", id, button, state)); + if (!gameControllers.contains(id)) + { + Ln.w("Received data for non-existant controller."); + break; + } + gameControllers.get(id).setButton(button, state); } break; case ControlMessage.TYPE_INJECT_GAME_CONTROLLER_DEVICE: - { + if (gameControllersEnabled) { int id = msg.getGameControllerId(); int event = msg.getGameControllerDeviceEvent(); - Ln.d(String.format("Received gc device event: %d %d", id, event)); + + switch (event) { + case GameController.DEVICE_ADDED: + gameControllers.append(id, new GameController()); + break; + + case GameController.DEVICE_REMOVED: + if (!gameControllers.contains(id)) + { + Ln.w("Non-existant game controller removed."); + break; + } + gameControllers.get(id).close(); + gameControllers.delete(id); + break; + } } break; default: 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 00000000..a83b3fd0 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/GameController.java @@ -0,0 +1,372 @@ +package com.genymobile.scrcpy; + +import java.io.*; +import java.util.Arrays; +import java.util.List; +import com.sun.jna.Library; +import com.sun.jna.Platform; +import com.sun.jna.Native; +import com.sun.jna.Structure; +import com.sun.jna.Pointer; + +public final class GameController { + public static final int DEVICE_ADDED = 0; + public static final int DEVICE_REMOVED = 1; + + private static int UINPUT_MAX_NAME_SIZE = 80; + + public static class input_id 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"); + } + } + + public static class uinput_setup extends Structure { + public input_id id; + public byte[] name = new byte[UINPUT_MAX_NAME_SIZE]; + public int ff_effects_max = 0; + + @Override + protected List getFieldOrder() { + return Arrays.asList("id", "name", "ff_effects_max"); + } + } + + public static class input_event32 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"); + } + } + + public static class input_event64 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 int _IOC_NONE = 0; + private static int _IOC_WRITE = 1; + + private static int _IOC_DIRSHIFT = 30; + private static int _IOC_TYPESHIFT = 8; + private static int _IOC_NRSHIFT = 0; + private static 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 _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_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_DEV_SETUP = _IOW(UINPUT_IOCTL_BASE, 3, new uinput_setup().size()); + 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; + + private static final short BTN_A = 0x130; + private static final short BTN_B = 0x131; + private static final short BTN_X = 0x133; + private static final short BTN_Y = 0x134; + private static final short BTN_TL = 0x136; + private static final short BTN_TR = 0x137; + private static final short BTN_SELECT = 0x13a; + private static final short BTN_START = 0x13b; + private static final short BTN_MODE = 0x13c; + private static final short BTN_THUMBL = 0x13d; + private static final short BTN_THUMBR = 0x13e; + + private static final short ABS_X = 0x00; + private static final short ABS_Y = 0x01; + private static final short ABS_Z = 0x02; + private static final short ABS_RX = 0x03; + private static final short ABS_RY = 0x04; + private static final short ABS_RZ = 0x05; + private static final short ABS_HAT0X = 0x10; + private static final short ABS_HAT0Y = 0x11; + + 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; + + private static final int SDL_CONTROLLER_AXIS_LEFTX = 0; + private static final int SDL_CONTROLLER_AXIS_LEFTY = 1; + private static final int SDL_CONTROLLER_AXIS_RIGHTX = 2; + private static final int SDL_CONTROLLER_AXIS_RIGHTY = 3; + private static final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4; + private static final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5; + + private static final int SDL_CONTROLLER_BUTTON_A = 0; + private static final int SDL_CONTROLLER_BUTTON_B = 1; + private static final int SDL_CONTROLLER_BUTTON_X = 2; + private static final int SDL_CONTROLLER_BUTTON_Y = 3; + private static final int SDL_CONTROLLER_BUTTON_BACK = 4; + private static final int SDL_CONTROLLER_BUTTON_GUIDE = 5; + private static final int SDL_CONTROLLER_BUTTON_START = 6; + private static final int SDL_CONTROLLER_BUTTON_LEFTSTICK = 7; + private static final int SDL_CONTROLLER_BUTTON_RIGHTSTICK = 8; + private static final int SDL_CONTROLLER_BUTTON_LEFTSHOULDER = 9; + private static final int SDL_CONTROLLER_BUTTON_RIGHTSHOULDER = 10; + private static final int SDL_CONTROLLER_BUTTON_DPAD_UP = 11; + private static final int SDL_CONTROLLER_BUTTON_DPAD_DOWN = 12; + private static final int SDL_CONTROLLER_BUTTON_DPAD_LEFT = 13; + private static final int SDL_CONTROLLER_BUTTON_DPAD_RIGHT = 14; + + private int fd; + + public interface LibC extends Library { + LibC fn = (LibC)Native.load("c", LibC.class); + + int open(String pathname, int flags); + int ioctl(int fd, long request, Object... args); + long write(int fd, Pointer buf, long count); + int close(int fd); + } + + static public void load_native_libraries() { + GameController.LibC.fn.write(1, null, 0); + } + + public GameController() { + fd = LibC.fn.open("/dev/uinput", O_WRONLY | O_NONBLOCK); + if (fd == -1) { + throw new RuntimeException("Couldn't open uinput device."); + } + + LibC.fn.ioctl(fd, UI_SET_EVBIT, EV_KEY); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_A); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_B); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_X); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_Y); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_BACK); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_START); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_LB); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_RB); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_GUIDE); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_LS); + LibC.fn.ioctl(fd, UI_SET_KEYBIT, XBOX_BTN_RS); + + LibC.fn.ioctl(fd, UI_SET_EVBIT, EV_ABS); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_LSX); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_LSY); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_RSX); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_RSY); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_DPADX); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_DPADY); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_LT); + LibC.fn.ioctl(fd, UI_SET_ABSBIT, XBOX_ABS_RT); + + uinput_setup usetup = new uinput_setup(); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0x045e; + usetup.id.product = 0x028e; + byte[] name = "Microsoft X-Box 360 pad".getBytes(); + System.arraycopy(name, 0, usetup.name, 0, name.length); + + if (LibC.fn.ioctl(fd, UI_DEV_SETUP, usetup) == -1) { + close(); + throw new RuntimeException("Couldn't setup uinput device."); + } + + if (LibC.fn.ioctl(fd, UI_DEV_CREATE) == -1) { + close(); + throw new RuntimeException("Couldn't create uinput device."); + } + } + + public void close() { + if (fd != -1) { + LibC.fn.ioctl(fd, UI_DEV_DESTROY); + LibC.fn.close(fd); + fd = -1; + } + } + + private static void emit32(int fd, short type, short code, int val) { + input_event32 ie = new input_event32(); + + ie.type = type; + ie.code = code; + ie.value = val; + + ie.write(); + + LibC.fn.write(fd, ie.getPointer(), ie.size()); + } + + private static void emit64(int fd, short type, short code, int val) { + input_event64 ie = new input_event64(); + + ie.type = type; + ie.code = code; + ie.value = val; + + ie.write(); + + LibC.fn.write(fd, ie.getPointer(), ie.size()); + } + + private static void emit(int fd, short type, short code, int val) { + if (Platform.is64Bit()) { + emit64(fd, type, code, val); + } else { + emit32(fd, type, code, val); + } + } + + 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) { + emit(fd, EV_ABS, translateAxis(axis), (value + 0x8000) >> 8); + emit(fd, EV_SYN, SYN_REPORT, 0); + } + + public void setButton(int button, int state) { + // DPad buttons are usually reported as axes + + switch (button) { + case SDL_CONTROLLER_BUTTON_DPAD_UP: + emit(fd, EV_ABS, XBOX_ABS_DPADY, state != 0 ? 0 : 127); + break; + + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + emit(fd, EV_ABS, XBOX_ABS_DPADY, state != 0 ? 255 : 127); + break; + + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + emit(fd, EV_ABS, XBOX_ABS_DPADX, state != 0 ? 0 : 127); + break; + + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + emit(fd, EV_ABS, XBOX_ABS_DPADX, state != 0 ? 255 : 127); + break; + + default: + emit(fd, EV_KEY, translateButton(button), state); + } + emit(fd, EV_SYN, SYN_REPORT, 0); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fdd9db88..a21a6db2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -8,12 +8,22 @@ import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; +import java.io.InputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Locale; +import java.util.concurrent.ExecutionException; 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 +31,23 @@ public final class Server { private static void scrcpy(Options options) throws IOException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); + Ln.i("Supported ABIs: " + String.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 stream = Server.class.getResourceAsStream("/lib/" + abi + "/" + lib); + Path destPath = Paths.get(SERVER_DIR + "/" + lib); + Files.copy(stream, destPath, StandardCopyOption.REPLACE_EXISTING); + 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()) {