Rewrite root input capturing to be compatible with Android 6.0 (and be much more secure in general)

This commit is contained in:
Cameron Gutman 2015-12-19 23:55:34 -08:00
parent 05f8fa21de
commit d6a8db97d8
8 changed files with 454 additions and 564 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
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<String, EvdevHandler> 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();
}
}
}
}

View File

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

View File

@ -1,5 +1,6 @@
#include <stdlib.h>
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
@ -8,34 +9,38 @@
#include <unistd.h>
#include <poll.h>
#include <errno.h>
#include <dirent.h>
#include <pthread.h>
#include <android/log.h>
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
const char *path;
#define REL_X 0x00
#define REL_Y 0x01
#define KEY_Q 16
#define BTN_LEFT 0x110
#define BTN_GAMEPAD 0x130
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
struct DeviceEntry {
struct DeviceEntry *next;
pthread_t thread;
int fd;
char devName[128];
};
return open(path, O_RDWR);
}
static struct DeviceEntry *DeviceListHead;
static int grabbing = 1;
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t StdoutLock = PTHREAD_MUTEX_INITIALIZER;
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 1) == 0;
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 0) == 0;
}
// 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);
@ -43,17 +48,7 @@ Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobje
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);
@ -61,58 +56,271 @@ Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject t
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;
struct pollfd pollinfo;
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);
}
data = (*env)->GetByteArrayElements(env, buffer, NULL);
if (data == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Failed to get byte array");
void* pollThreadFunc(void* context) {
struct DeviceEntry *device = context;
struct pollfd pollinfo;
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(&currentEntry->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);
// 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;
}
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);
}
}
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);
}
startPollForDevice(dirEnt->d_name);
}
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
return ret;
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);
}
}
}