From d6a8db97d8502dd50bc15f0bf6947bde0b688490 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 19 Dec 2015 23:55:34 -0800 Subject: [PATCH] Rewrite root input capturing to be compatible with Android 6.0 (and be much more secure in general) --- app/src/main/java/com/limelight/Game.java | 23 +- .../binding/input/evdev/EvdevEvent.java | 4 - .../binding/input/evdev/EvdevHandler.java | 179 +++++---- .../binding/input/evdev/EvdevReader.java | 115 ++---- .../binding/input/evdev/EvdevShell.java | 116 ------ .../binding/input/evdev/EvdevWatcher.java | 188 --------- app/src/main/jni/evdev_reader/Android.mk | 21 +- app/src/main/jni/evdev_reader/evdev_reader.c | 372 ++++++++++++++---- 8 files changed, 454 insertions(+), 564 deletions(-) delete mode 100644 app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java delete mode 100644 app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 169ab998..71ffbf30 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -6,8 +6,8 @@ import com.limelight.binding.input.ControllerHandler; import com.limelight.binding.input.KeyboardTranslator; import com.limelight.binding.input.TouchContext; import com.limelight.binding.input.driver.UsbDriverService; +import com.limelight.binding.input.evdev.EvdevHandler; import com.limelight.binding.input.evdev.EvdevListener; -import com.limelight.binding.input.evdev.EvdevWatcher; import com.limelight.binding.video.EnhancedDecoderRenderer; import com.limelight.binding.video.MediaCodecDecoderRenderer; import com.limelight.binding.video.MediaCodecHelper; @@ -89,7 +89,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, private boolean connected = false; private boolean deferredSurfaceResize = false; - private EvdevWatcher evdevWatcher; + private EvdevHandler evdevHandler; private int modifierFlags = 0; private boolean grabbedInput = true; private boolean grabComboDown = false; @@ -280,8 +280,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, if (LimelightBuildProps.ROOT_BUILD) { // Start watching for raw input - evdevWatcher = new EvdevWatcher(this); - evdevWatcher.start(); + evdevHandler = new EvdevHandler(this, this); + evdevHandler.start(); } if (prefConfig.usbDriver) { @@ -402,13 +402,12 @@ public class Game extends Activity implements SurfaceHolder.Callback, private final Runnable toggleGrab = new Runnable() { @Override public void run() { - - if (evdevWatcher != null) { + if (evdevHandler != null) { if (grabbedInput) { - evdevWatcher.ungrabAll(); + evdevHandler.ungrabAll(); } else { - evdevWatcher.regrabAll(); + evdevHandler.regrabAll(); } } @@ -796,10 +795,10 @@ public class Game extends Activity implements SurfaceHolder.Callback, conn.stop(); } - // Close the Evdev watcher to allow use of captured input devices - if (evdevWatcher != null) { - evdevWatcher.shutdown(); - evdevWatcher = null; + // Close the Evdev reader to allow use of captured input devices + if (evdevHandler != null) { + evdevHandler.stop(); + evdevHandler = null; } } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java index 5e04f168..30878bbc 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevEvent.java @@ -24,10 +24,6 @@ public class EvdevEvent { public static final short BTN_FORWARD = 0x115; public static final short BTN_BACK = 0x116; public static final short BTN_TASK = 0x117; - public static final short BTN_GAMEPAD = 0x130; - - /* Keys */ - public static final short KEY_Q = 16; public final short type; public final short code; diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java index 0a164ff9..9bf5157b 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevHandler.java @@ -1,76 +1,71 @@ package com.limelight.binding.input.evdev; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; +import android.content.Context; -import com.limelight.LimeLog; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; public class EvdevHandler { - private final String absolutePath; private final EvdevListener listener; + private final String libraryPath; + private boolean shutdown = false; - private int fd = -1; + private InputStream evdevIn; + private OutputStream evdevOut; + private Process reader; + + private static final byte UNGRAB_REQUEST = 1; + private static final byte REGRAB_REQUEST = 2; private final Thread handlerThread = new Thread() { @Override public void run() { - // All the finally blocks here make this code look like a mess - // but it's important that we get this right to avoid causing - // system-wide input problems. + int deltaX = 0; + int deltaY = 0; + byte deltaScroll = 0; - // Open the /dev/input/eventX file - fd = EvdevReader.open(absolutePath); - if (fd == -1) { - LimeLog.warning("Unable to open "+absolutePath); + // Launch the evdev reader shell + ProcessBuilder builder = new ProcessBuilder("su", "-c", libraryPath+File.separatorChar+"libevdev_reader.so"); + builder.redirectErrorStream(false); + + try { + reader = builder.start(); + } catch (IOException e) { + e.printStackTrace(); return; } - try { - // Check if it's a mouse or keyboard, but not a gamepad - if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) || - EvdevReader.isGamepad(fd)) { - // We only handle keyboards and mice - return; - } - - // Grab it for ourselves - if (!EvdevReader.grab(fd)) { - LimeLog.warning("Unable to grab "+absolutePath); - return; - } - - LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath); - - ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder()); + evdevIn = reader.getInputStream(); + evdevOut = reader.getOutputStream(); + while (!isInterrupted() && !shutdown) { + EvdevEvent event; try { - int deltaX = 0; - int deltaY = 0; - byte deltaScroll = 0; + event = EvdevReader.read(evdevIn); + } catch (IOException e) { + event = null; + } + if (event == null) { + break; + } - while (!isInterrupted() && !shutdown) { - EvdevEvent event = EvdevReader.read(fd, buffer); - if (event == null) { - return; + switch (event.type) { + case EvdevEvent.EV_SYN: + if (deltaX != 0 || deltaY != 0) { + listener.mouseMove(deltaX, deltaY); + deltaX = deltaY = 0; } + if (deltaScroll != 0) { + listener.mouseScroll(deltaScroll); + deltaScroll = 0; + } + break; - switch (event.type) - { - case EvdevEvent.EV_SYN: - if (deltaX != 0 || deltaY != 0) { - listener.mouseMove(deltaX, deltaY); - deltaX = deltaY = 0; - } - if (deltaScroll != 0) { - listener.mouseScroll(deltaScroll); - deltaScroll = 0; - } - break; - - case EvdevEvent.EV_REL: - switch (event.code) - { + case EvdevEvent.EV_REL: + switch (event.code) { case EvdevEvent.REL_X: deltaX = event.value; break; @@ -80,12 +75,11 @@ public class EvdevHandler { case EvdevEvent.REL_WHEEL: deltaScroll = (byte) event.value; break; - } - break; + } + break; - case EvdevEvent.EV_KEY: - switch (event.code) - { + case EvdevEvent.EV_KEY: + switch (event.code) { case EvdevEvent.BTN_LEFT: listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, event.value != 0); @@ -118,27 +112,39 @@ public class EvdevHandler { listener.keyboardEvent(event.value != 0, keyCode); } break; - } - break; - - case EvdevEvent.EV_MSC: - break; } - } - } finally { - // Release our grab - EvdevReader.ungrab(fd); + break; + + case EvdevEvent.EV_MSC: + break; } - } finally { - // Close the file - EvdevReader.close(fd); } } }; - public EvdevHandler(String absolutePath, EvdevListener listener) { - this.absolutePath = absolutePath; + public EvdevHandler(Context context, EvdevListener listener) { this.listener = listener; + this.libraryPath = context.getApplicationInfo().nativeLibraryDir; + } + + public void regrabAll() { + if (!shutdown && evdevOut != null) { + try { + evdevOut.write(REGRAB_REQUEST); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void ungrabAll() { + if (!shutdown && evdevOut != null) { + try { + evdevOut.write(UNGRAB_REQUEST); + } catch (IOException e) { + e.printStackTrace(); + } + } } public void start() { @@ -146,11 +152,28 @@ public class EvdevHandler { } public void stop() { - // Close the fd. It doesn't matter if this races - // with the handler thread. We'll close this out from - // under the thread to wake it up - if (fd != -1) { - EvdevReader.close(fd); + // We need to stop the process in this context otherwise + // we could get stuck waiting on output from the process + // in order to terminate it. + + if (evdevIn != null) { + try { + evdevIn.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (evdevOut != null) { + try { + evdevOut.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (reader != null) { + reader.destroy(); } shutdown = true; @@ -160,8 +183,4 @@ public class EvdevHandler { handlerThread.join(); } catch (InterruptedException ignored) {} } - - public void notifyDeleted() { - stop(); - } } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java index f6c4b0e5..e317e12d 100644 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java +++ b/app/src/main/java/com/limelight/binding/input/evdev/EvdevReader.java @@ -1,105 +1,58 @@ package com.limelight.binding.input.evdev; -import android.os.Build; - +import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; -import java.util.Locale; +import java.nio.ByteOrder; import com.limelight.LimeLog; public class EvdevReader { - static { - System.loadLibrary("evdev_reader"); - } + private static void readAll(InputStream in, ByteBuffer bb) throws IOException { + byte[] buf = bb.array(); + int ret; + int offset = 0; - public static void patchSeLinuxPolicies() { - // - // FIXME: We REALLY shouldn't being changing permissions on the input devices like this. - // We should probably do something clever with a separate daemon and talk via a localhost - // socket. We don't return the SELinux policies back to default after we're done which I feel - // bad about, but we do chmod the input devices back so I don't think any additional attack surface - // remains opened after streaming other than listing the /dev/input directory which you wouldn't - // normally be able to do with SELinux enforcing on Lollipop. - // - // We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other - // more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on - // 4.4 and later to do live SELinux policy changes. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - EvdevShell shell = EvdevShell.getInstance(); - shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " + - "\"allow untrusted_app input_device chr_file { open read write ioctl }\""); + while (offset < buf.length) { + ret = in.read(buf, offset, buf.length-offset); + if (ret <= 0) { + throw new IOException("Read failed: "+ret); + } + + offset += ret; } } - // Requires root to chmod /dev/input/eventX - public static void setPermissions(String[] files, int octalPermissions) { - EvdevShell shell = EvdevShell.getInstance(); - - for (String file : files) { - shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file)); - } - } - - // Returns the fd to be passed to other function or -1 on error - public static native int open(String fileName); - - // Prevent other apps (including Android itself) from using the device while "grabbed" - public static native boolean grab(int fd); - public static native boolean ungrab(int fd); - - // Used for checking device capabilities - public static native boolean hasRelAxis(int fd, short axis); - public static native boolean hasAbsAxis(int fd, short axis); - public static native boolean hasKey(int fd, short key); - - public static boolean isMouse(int fd) { - // This is the same check that Android does in EventHub.cpp - return hasRelAxis(fd, EvdevEvent.REL_X) && - hasRelAxis(fd, EvdevEvent.REL_Y) && - hasKey(fd, EvdevEvent.BTN_LEFT); - } - - public static boolean isAlphaKeyboard(int fd) { - // This is the same check that Android does in EventHub.cpp - return hasKey(fd, EvdevEvent.KEY_Q); - } - - public static boolean isGamepad(int fd) { - return hasKey(fd, EvdevEvent.BTN_GAMEPAD); - } - - // Returns the bytes read or -1 on error - private static native int read(int fd, byte[] buffer); - // Takes a byte buffer to use to read the output into. // This buffer MUST be in native byte order and at least // EVDEV_MAX_EVENT_SIZE bytes long. - public static EvdevEvent read(int fd, ByteBuffer buffer) { - int bytesRead = read(fd, buffer.array()); - if (bytesRead < 0) { - LimeLog.warning("Failed to read: "+bytesRead); - return null; - } - else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) { - LimeLog.warning("Short read: "+bytesRead); + public static EvdevEvent read(InputStream input) throws IOException { + ByteBuffer bb; + int packetLength; + + // Read the packet length + bb = ByteBuffer.allocate(4).order(ByteOrder.nativeOrder()); + readAll(input, bb); + packetLength = bb.getInt(); + + if (packetLength < EvdevEvent.EVDEV_MIN_EVENT_SIZE) { + LimeLog.warning("Short read: "+packetLength); return null; } - buffer.limit(bytesRead); - buffer.rewind(); + // Read the rest of the packet + bb = ByteBuffer.allocate(packetLength).order(ByteOrder.nativeOrder()); + readAll(input, bb); // Throw away the time stamp - if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { - buffer.getLong(); - buffer.getLong(); + if (packetLength == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { + bb.getLong(); + bb.getLong(); } else { - buffer.getInt(); - buffer.getInt(); + bb.getInt(); + bb.getInt(); } - return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt()); + return new EvdevEvent(bb.getShort(), bb.getShort(), bb.getInt()); } - - // Closes the fd from open() - public static native int close(int fd); } diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java deleted file mode 100644 index 1ac5f51a..00000000 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevShell.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.limelight.binding.input.evdev; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Scanner; -import java.util.UUID; - -public class EvdevShell { - private OutputStream stdin; - private InputStream stdout; - private Process shell; - private final String uuidString = UUID.randomUUID().toString(); - - private static final EvdevShell globalShell = new EvdevShell(); - - public static EvdevShell getInstance() { - return globalShell; - } - - public void startShell() { - ProcessBuilder builder = new ProcessBuilder("su"); - - try { - // Redirect stderr to stdout - builder.redirectErrorStream(true); - shell = builder.start(); - - stdin = shell.getOutputStream(); - stdout = shell.getInputStream(); - } catch (IOException e) { - // This is unexpected - e.printStackTrace(); - - // Kill the shell if it spawned - if (stdin != null) { - try { - stdin.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } finally { - stdin = null; - } - } - if (stdout != null) { - try { - stdout.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } finally { - stdout = null; - } - } - if (shell != null) { - shell.destroy(); - shell = null; - } - } - } - - public void runCommand(String command) { - if (shell == null) { - // Shell never started - return; - } - - try { - // Write the command followed by an echo with our UUID - stdin.write((command+'\n').getBytes("UTF-8")); - stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8")); - stdin.flush(); - - // This is the only command in flight so we can use a scanner - // without worrying about it eating too many characters - Scanner scanner = new Scanner(stdout); - while (scanner.hasNext()) { - if (scanner.next().contains(uuidString)) { - // Our command ran - return; - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void stopShell() throws InterruptedException { - boolean exitWritten = false; - - if (shell == null) { - // Shell never started - return; - } - - try { - stdin.write("exit\n".getBytes("UTF-8")); - exitWritten = true; - } catch (IOException e) { - // We'll destroy the process without - // waiting for it to terminate since - // we don't know whether our exit command made it - e.printStackTrace(); - } - - if (exitWritten) { - try { - shell.waitFor(); - } finally { - shell.destroy(); - } - } - else { - shell.destroy(); - } - } -} diff --git a/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java b/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java deleted file mode 100644 index 731168e0..00000000 --- a/app/src/main/java/com/limelight/binding/input/evdev/EvdevWatcher.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.limelight.binding.input.evdev; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -import com.limelight.LimeLog; - -import android.os.FileObserver; - -@SuppressWarnings("ALL") -public class EvdevWatcher { - private static final String PATH = "/dev/input"; - private static final String REQUIRED_FILE_PREFIX = "event"; - - private final HashMap handlers = new HashMap(); - private boolean shutdown = false; - private boolean init = false; - private boolean ungrabbed = false; - private EvdevListener listener; - private Thread startThread; - - private static boolean patchedSeLinuxPolicies = false; - - private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { - @Override - public void onEvent(int event, String fileName) { - if (fileName == null) { - return; - } - - if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { - return; - } - - synchronized (handlers) { - if (shutdown) { - return; - } - - if ((event & FileObserver.CREATE) != 0) { - LimeLog.info("Starting evdev handler for "+fileName); - - if (!init) { - // If this a real new device, update permissions again so we can read it - EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666); - } - - EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); - - // If we're ungrabbed now, don't start the handler - if (!ungrabbed) { - handler.start(); - } - - handlers.put(fileName, handler); - } - - if ((event & FileObserver.DELETE) != 0) { - LimeLog.info("Halting evdev handler for "+fileName); - - EvdevHandler handler = handlers.remove(fileName); - if (handler != null) { - handler.notifyDeleted(); - } - } - } - } - }; - - public EvdevWatcher(EvdevListener listener) { - this.listener = listener; - } - - private File[] rundownWithPermissionsChange(int newPermissions) { - // Rundown existing files - File devInputDir = new File(PATH); - File[] files = devInputDir.listFiles(); - if (files == null) { - return new File[0]; - } - - // Set desired permissions - String[] filePaths = new String[files.length]; - for (int i = 0; i < files.length; i++) { - filePaths[i] = files[i].getAbsolutePath(); - } - EvdevReader.setPermissions(filePaths, newPermissions); - - return files; - } - - public void ungrabAll() { - synchronized (handlers) { - // Note that we're ungrabbed for now - ungrabbed = true; - - // Stop all handlers - for (EvdevHandler handler : handlers.values()) { - handler.stop(); - } - } - } - - public void regrabAll() { - synchronized (handlers) { - // We're regrabbing everything now - ungrabbed = false; - - for (Map.Entry entry : handlers.entrySet()) { - // We need to recreate each entry since we can't reuse a stopped one - entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener)); - entry.getValue().start(); - } - } - } - - public void start() { - startThread = new Thread() { - @Override - public void run() { - // Initialize the root shell - EvdevShell.getInstance().startShell(); - - // Patch SELinux policies (if needed) - if (!patchedSeLinuxPolicies) { - EvdevReader.patchSeLinuxPolicies(); - patchedSeLinuxPolicies = true; - } - - // List all files and allow us access - File[] files = rundownWithPermissionsChange(0666); - - init = true; - for (File f : files) { - observer.onEvent(FileObserver.CREATE, f.getName()); - } - - // Done with initial onEvent calls - init = false; - - // Start watching for new files - observer.startWatching(); - - synchronized (startThread) { - // Wait to be awoken again by shutdown() - try { - startThread.wait(); - } catch (InterruptedException e) {} - } - - // Giveup eventX permissions - rundownWithPermissionsChange(0660); - - // Kill the root shell - try { - EvdevShell.getInstance().stopShell(); - } catch (InterruptedException e) {} - } - }; - startThread.start(); - } - - public void shutdown() { - // Let start thread cleanup on it's own sweet time - synchronized (startThread) { - startThread.notify(); - } - - // Stop the observer - observer.stopWatching(); - - synchronized (handlers) { - // Stop creating new handlers - shutdown = true; - - // If we've already ungrabbed, there's nothing else to do - if (ungrabbed) { - return; - } - - // Stop all handlers - for (EvdevHandler handler : handlers.values()) { - handler.stop(); - } - } - } -} diff --git a/app/src/main/jni/evdev_reader/Android.mk b/app/src/main/jni/evdev_reader/Android.mk index e67e41da..2de922d1 100644 --- a/app/src/main/jni/evdev_reader/Android.mk +++ b/app/src/main/jni/evdev_reader/Android.mk @@ -10,4 +10,23 @@ LOCAL_MODULE := evdev_reader LOCAL_SRC_FILES := evdev_reader.c LOCAL_LDLIBS := -llog -include $(BUILD_SHARED_LIBRARY) + +# This next portion of the makefile is mostly copied from build-executable.mk but +# creates a binary with the libXXX.so form so the APK will install and drop +# the binary correctly. + +LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE +LOCAL_MAKEFILE := $(local-makefile) + +$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT)) +$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE)) +$(call check-LOCAL_MODULE_FILENAME) + +# we are building target objects +my := TARGET_ + +$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION)) +$(call handle-module-built) + +LOCAL_MODULE_CLASS := EXECUTABLE +include $(BUILD_SYSTEM)/build-module.mk diff --git a/app/src/main/jni/evdev_reader/evdev_reader.c b/app/src/main/jni/evdev_reader/evdev_reader.c index fb0c7d39..2001c73d 100644 --- a/app/src/main/jni/evdev_reader/evdev_reader.c +++ b/app/src/main/jni/evdev_reader/evdev_reader.c @@ -1,5 +1,6 @@ #include -#include +#include +#include #include #include @@ -8,111 +9,318 @@ #include #include #include +#include +#include #include -JNIEXPORT jint JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) { - const char *path; - - path = (*env)->GetStringUTFChars(env, absolutePath, NULL); - - return open(path, O_RDWR); -} +#define REL_X 0x00 +#define REL_Y 0x01 +#define KEY_Q 16 +#define BTN_LEFT 0x110 +#define BTN_GAMEPAD 0x130 -JNIEXPORT jboolean JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) { - return ioctl(fd, EVIOCGRAB, 1) == 0; -} +struct DeviceEntry { + struct DeviceEntry *next; + pthread_t thread; + int fd; + char devName[128]; +}; -JNIEXPORT jboolean JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) { - return ioctl(fd, EVIOCGRAB, 0) == 0; -} +static struct DeviceEntry *DeviceListHead; +static int grabbing = 1; +static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t StdoutLock = PTHREAD_MUTEX_INITIALIZER; -// has*() and friends are based on Android's EventHub.cpp +// This is a small executable that runs in a root shell. It reads input +// devices and writes the evdev output packets to stdout. This allows +// Moonlight to read input devices without having to muck with changing +// device permissions or modifying SELinux policy (which is prevented in +// Marshmallow anyway). #define test_bit(bit, array) (array[bit/8] & (1<<(bit%8))) -JNIEXPORT jboolean JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) { +static int hasRelAxis(int fd, short axis) { unsigned char relBitmask[(REL_MAX + 1) / 8]; - + ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask); - + return test_bit(axis, relBitmask); } -JNIEXPORT jboolean JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) { - unsigned char absBitmask[(ABS_MAX + 1) / 8]; - - ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask); - - return test_bit(axis, absBitmask); -} - -JNIEXPORT jboolean JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) { +static int hasKey(int fd, short key) { unsigned char keyBitmask[(KEY_MAX + 1) / 8]; - + ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask); - + return test_bit(key, keyBitmask); } -JNIEXPORT jint JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) { - jint ret; - jbyte *data; - int pollres; +static void outputEvdevData(char *data, int dataSize) { + // We need to hold our StdoutLock to avoid garbling + // input data when multiple threads try to write at once. + pthread_mutex_lock(&StdoutLock); + fwrite(&dataSize, sizeof(dataSize), 1, stdout); + fwrite(data, dataSize, 1, stdout); + fflush(stdout); + pthread_mutex_unlock(&StdoutLock); +} + +void* pollThreadFunc(void* context) { + struct DeviceEntry *device = context; struct pollfd pollinfo; - - data = (*env)->GetByteArrayElements(env, buffer, NULL); - if (data == NULL) { - __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", - "Failed to get byte array"); + int pollres, ret; + char data[64]; + + __android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName); + + if (grabbing) { + // Exclusively grab the input device (required to make the Android cursor disappear) + if (ioctl(device->fd, EVIOCGRAB, 1) < 0) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", + "EVIOCGRAB failed for %s: %d", device->devName, errno); + goto cleanup; + } + } + + for (;;) { + do { + // Unwait every 250 ms to return to caller if the fd is closed + pollinfo.fd = device->fd; + pollinfo.events = POLLIN; + pollinfo.revents = 0; + pollres = poll(&pollinfo, 1, 250); + } + while (pollres == 0); + + if (pollres > 0 && (pollinfo.revents & POLLIN)) { + // We'll have data available now + ret = read(device->fd, data, sizeof(struct input_event)); + if (ret < 0) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", + "read() failed: %d", errno); + goto cleanup; + } + else if (ret == 0) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", + "read() graceful EOF"); + goto cleanup; + } + else if (grabbing) { + // Write out the data to our client + outputEvdevData(data, ret); + } + } + else { + if (pollres < 0) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", + "poll() failed: %d", errno); + } + else { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", + "Unexpected revents: %d", pollinfo.revents); + } + + // Terminate this thread + goto cleanup; + } + } + +cleanup: + __android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Closing /dev/input/%s", device->devName); + + // Remove the context from the linked list + { + struct DeviceEntry *lastEntry; + + if (DeviceListHead == device) { + DeviceListHead = device->next; + } + else { + lastEntry = DeviceListHead; + while (lastEntry->next != NULL) { + if (lastEntry->next == device) { + lastEntry->next = device->next; + break; + } + + lastEntry = lastEntry->next; + } + } + } + + // Free the context + ioctl(device->fd, EVIOCGRAB, 0); + close(device->fd); + free(device); + + return NULL; +} + +static int precheckDeviceForPolling(int fd) { + int isMouse; + int isKeyboard; + int isGamepad; + + // This is the same check that Android does in EventHub.cpp + isMouse = hasRelAxis(fd, REL_X) && + hasRelAxis(fd, REL_Y) && + hasKey(fd, BTN_LEFT); + + // This is the same check that Android does in EventHub.cpp + isKeyboard = hasKey(fd, KEY_Q); + + isGamepad = hasKey(fd, BTN_GAMEPAD); + + // We only handle keyboards and mice that aren't gamepads + return (isMouse || isKeyboard) && !isGamepad; +} + +static void startPollForDevice(char* deviceName) { + struct DeviceEntry *currentEntry; + char fullPath[256]; + int fd; + + // Lock the device list + pthread_mutex_lock(&DeviceListLock); + + // Check if the device is already being polled + currentEntry = DeviceListHead; + while (currentEntry != NULL) { + if (strcmp(currentEntry->devName, deviceName) == 0) { + // Already polling this device + goto unlock; + } + + currentEntry = currentEntry->next; + } + + // Open the device + sprintf(fullPath, "/dev/input/%s", deviceName); + fd = open(fullPath, O_RDWR); + if (fd < 0) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open %s: %d", fullPath, errno); + goto unlock; + } + + // Allocate a context + currentEntry = malloc(sizeof(*currentEntry)); + if (currentEntry == NULL) { + close(fd); + goto unlock; + } + + // Populate context + currentEntry->fd = fd; + strcpy(currentEntry->devName, deviceName); + + // Check if we support polling this device + if (!precheckDeviceForPolling(fd)) { + // Nope, get out + free(currentEntry); + close(fd); + goto unlock; + } + + // Start the polling thread + if (pthread_create(¤tEntry->thread, NULL, pollThreadFunc, currentEntry) != 0) { + free(currentEntry); + close(fd); + goto unlock; + } + + // Queue this onto the device list + currentEntry->next = DeviceListHead; + DeviceListHead = currentEntry; + +unlock: + // Unlock and return + pthread_mutex_unlock(&DeviceListLock); +} + +static int enumerateDevices(void) { + DIR *inputDir; + struct dirent *dirEnt; + + inputDir = opendir("/dev/input"); + if (!inputDir) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open /dev/input: %d", errno); return -1; } - do - { - // Unwait every 250 ms to return to caller if the fd is closed - pollinfo.fd = fd; - pollinfo.events = POLLIN; - pollinfo.revents = 0; - pollres = poll(&pollinfo, 1, 250); - } - while (pollres == 0); - - if (pollres > 0 && (pollinfo.revents & POLLIN)) { - // We'll have data available now - ret = read(fd, data, sizeof(struct input_event)); - if (ret < 0) { - __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", - "read() failed: %d", errno); + // Start polling each device in /dev/input + while ((dirEnt = readdir(inputDir)) != NULL) { + if (strcmp(dirEnt->d_name, ".") == 0 || strcmp(dirEnt->d_name, "..") == 0) { + // Skip these virtual directories + continue; } - } - else { - // There must have been a failure - ret = -1; - - if (pollres < 0) { - __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", - "poll() failed: %d", errno); - } - else { - __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", - "Unexpected revents: %d", pollinfo.revents); - } - } - - (*env)->ReleaseByteArrayElements(env, buffer, data, 0); - return ret; + startPollForDevice(dirEnt->d_name); + } + + closedir(inputDir); + return 0; } -JNIEXPORT jint JNICALL -Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) { - return close(fd); -} +#define UNGRAB_REQ 1 +#define REGRAB_REQ 2 + +int main(int argc, char* argv[]) { + int ret; + int pollres; + struct pollfd pollinfo; + + // Perform initial enumeration + ret = enumerateDevices(); + if (ret < 0) { + return ret; + } + + // Wait for requests from the client + for (;;) { + unsigned char requestId; + + do { + // Every second we poll again for new devices if + // we haven't received any new events + pollinfo.fd = STDIN_FILENO; + pollinfo.events = POLLIN; + pollinfo.revents = 0; + pollres = poll(&pollinfo, 1, 1000); + if (pollres == 0) { + // Timeout, re-enumerate devices + enumerateDevices(); + } + } + while (pollres == 0); + + ret = fread(&requestId, sizeof(requestId), 1, stdin); + if (ret < sizeof(requestId)) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on input"); + return errno; + } + + if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) { + __android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request"); + return requestId; + } + + { + struct DeviceEntry *currentEntry; + + pthread_mutex_lock(&DeviceListLock); + + // Update state for future devices + grabbing = (requestId == REGRAB_REQ); + + // Carry out the requested action on each device + currentEntry = DeviceListHead; + while (currentEntry != NULL) { + ioctl(currentEntry->fd, EVIOCGRAB, grabbing); + currentEntry = currentEntry->next; + } + + pthread_mutex_unlock(&DeviceListLock); + } + } +} \ No newline at end of file