Use uinput to emulate game controllers

This commit is contained in:
Luiz Henrique Laurini 2021-02-17 21:36:44 -03:00
parent 0ba031b183
commit 12777f5812
7 changed files with 459 additions and 8 deletions

View file

@ -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"

View file

@ -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'
}

View file

@ -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 *; }

View file

@ -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

View file

@ -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<GameController> gameControllers = new SparseArray<GameController>();
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:

View file

@ -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<String> 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<String> 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<String> 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<String> 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);
}
}

View file

@ -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<CodecOption> 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()) {