mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-23 13:02:45 +00:00
Rewrite root input capturing to be compatible with Android 6.0 (and be much more secure in general)
This commit is contained in:
parent
05f8fa21de
commit
d6a8db97d8
@ -6,8 +6,8 @@ import com.limelight.binding.input.ControllerHandler;
|
|||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
import com.limelight.binding.input.TouchContext;
|
import com.limelight.binding.input.TouchContext;
|
||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
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.EvdevListener;
|
||||||
import com.limelight.binding.input.evdev.EvdevWatcher;
|
|
||||||
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
||||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
@ -89,7 +89,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
private boolean deferredSurfaceResize = false;
|
private boolean deferredSurfaceResize = false;
|
||||||
|
|
||||||
private EvdevWatcher evdevWatcher;
|
private EvdevHandler evdevHandler;
|
||||||
private int modifierFlags = 0;
|
private int modifierFlags = 0;
|
||||||
private boolean grabbedInput = true;
|
private boolean grabbedInput = true;
|
||||||
private boolean grabComboDown = false;
|
private boolean grabComboDown = false;
|
||||||
@ -280,8 +280,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
if (LimelightBuildProps.ROOT_BUILD) {
|
if (LimelightBuildProps.ROOT_BUILD) {
|
||||||
// Start watching for raw input
|
// Start watching for raw input
|
||||||
evdevWatcher = new EvdevWatcher(this);
|
evdevHandler = new EvdevHandler(this, this);
|
||||||
evdevWatcher.start();
|
evdevHandler.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefConfig.usbDriver) {
|
if (prefConfig.usbDriver) {
|
||||||
@ -402,13 +402,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private final Runnable toggleGrab = new Runnable() {
|
private final Runnable toggleGrab = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if (evdevHandler != null) {
|
||||||
if (evdevWatcher != null) {
|
|
||||||
if (grabbedInput) {
|
if (grabbedInput) {
|
||||||
evdevWatcher.ungrabAll();
|
evdevHandler.ungrabAll();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
evdevWatcher.regrabAll();
|
evdevHandler.regrabAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -796,10 +795,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
conn.stop();
|
conn.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the Evdev watcher to allow use of captured input devices
|
// Close the Evdev reader to allow use of captured input devices
|
||||||
if (evdevWatcher != null) {
|
if (evdevHandler != null) {
|
||||||
evdevWatcher.shutdown();
|
evdevHandler.stop();
|
||||||
evdevWatcher = null;
|
evdevHandler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,10 +24,6 @@ public class EvdevEvent {
|
|||||||
public static final short BTN_FORWARD = 0x115;
|
public static final short BTN_FORWARD = 0x115;
|
||||||
public static final short BTN_BACK = 0x116;
|
public static final short BTN_BACK = 0x116;
|
||||||
public static final short BTN_TASK = 0x117;
|
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 type;
|
||||||
public final short code;
|
public final short code;
|
||||||
|
@ -1,62 +1,58 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import android.content.Context;
|
||||||
import java.nio.ByteOrder;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class EvdevHandler {
|
public class EvdevHandler {
|
||||||
|
|
||||||
private final String absolutePath;
|
|
||||||
private final EvdevListener listener;
|
private final EvdevListener listener;
|
||||||
|
private final String libraryPath;
|
||||||
|
|
||||||
private boolean shutdown = false;
|
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() {
|
private final Thread handlerThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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.
|
|
||||||
|
|
||||||
// Open the /dev/input/eventX file
|
|
||||||
fd = EvdevReader.open(absolutePath);
|
|
||||||
if (fd == -1) {
|
|
||||||
LimeLog.warning("Unable to open "+absolutePath);
|
|
||||||
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());
|
|
||||||
|
|
||||||
try {
|
|
||||||
int deltaX = 0;
|
int deltaX = 0;
|
||||||
int deltaY = 0;
|
int deltaY = 0;
|
||||||
byte deltaScroll = 0;
|
byte deltaScroll = 0;
|
||||||
|
|
||||||
while (!isInterrupted() && !shutdown) {
|
// Launch the evdev reader shell
|
||||||
EvdevEvent event = EvdevReader.read(fd, buffer);
|
ProcessBuilder builder = new ProcessBuilder("su", "-c", libraryPath+File.separatorChar+"libevdev_reader.so");
|
||||||
if (event == null) {
|
builder.redirectErrorStream(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
reader = builder.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.type)
|
evdevIn = reader.getInputStream();
|
||||||
{
|
evdevOut = reader.getOutputStream();
|
||||||
|
|
||||||
|
while (!isInterrupted() && !shutdown) {
|
||||||
|
EvdevEvent event;
|
||||||
|
try {
|
||||||
|
event = EvdevReader.read(evdevIn);
|
||||||
|
} catch (IOException e) {
|
||||||
|
event = null;
|
||||||
|
}
|
||||||
|
if (event == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
case EvdevEvent.EV_SYN:
|
case EvdevEvent.EV_SYN:
|
||||||
if (deltaX != 0 || deltaY != 0) {
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
listener.mouseMove(deltaX, deltaY);
|
listener.mouseMove(deltaX, deltaY);
|
||||||
@ -69,8 +65,7 @@ public class EvdevHandler {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case EvdevEvent.EV_REL:
|
case EvdevEvent.EV_REL:
|
||||||
switch (event.code)
|
switch (event.code) {
|
||||||
{
|
|
||||||
case EvdevEvent.REL_X:
|
case EvdevEvent.REL_X:
|
||||||
deltaX = event.value;
|
deltaX = event.value;
|
||||||
break;
|
break;
|
||||||
@ -84,8 +79,7 @@ public class EvdevHandler {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case EvdevEvent.EV_KEY:
|
case EvdevEvent.EV_KEY:
|
||||||
switch (event.code)
|
switch (event.code) {
|
||||||
{
|
|
||||||
case EvdevEvent.BTN_LEFT:
|
case EvdevEvent.BTN_LEFT:
|
||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||||
event.value != 0);
|
event.value != 0);
|
||||||
@ -125,20 +119,32 @@ public class EvdevHandler {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
// Release our grab
|
|
||||||
EvdevReader.ungrab(fd);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// Close the file
|
|
||||||
EvdevReader.close(fd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public EvdevHandler(String absolutePath, EvdevListener listener) {
|
public EvdevHandler(Context context, EvdevListener listener) {
|
||||||
this.absolutePath = absolutePath;
|
|
||||||
this.listener = 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() {
|
public void start() {
|
||||||
@ -146,11 +152,28 @@ public class EvdevHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
// Close the fd. It doesn't matter if this races
|
// We need to stop the process in this context otherwise
|
||||||
// with the handler thread. We'll close this out from
|
// we could get stuck waiting on output from the process
|
||||||
// under the thread to wake it up
|
// in order to terminate it.
|
||||||
if (fd != -1) {
|
|
||||||
EvdevReader.close(fd);
|
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;
|
shutdown = true;
|
||||||
@ -160,8 +183,4 @@ public class EvdevHandler {
|
|||||||
handlerThread.join();
|
handlerThread.join();
|
||||||
} catch (InterruptedException ignored) {}
|
} catch (InterruptedException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyDeleted() {
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,105 +1,58 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
import android.os.Build;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Locale;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
public class EvdevReader {
|
public class EvdevReader {
|
||||||
static {
|
private static void readAll(InputStream in, ByteBuffer bb) throws IOException {
|
||||||
System.loadLibrary("evdev_reader");
|
byte[] buf = bb.array();
|
||||||
|
int ret;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
while (offset < buf.length) {
|
||||||
|
ret = in.read(buf, offset, buf.length-offset);
|
||||||
|
if (ret <= 0) {
|
||||||
|
throw new IOException("Read failed: "+ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void patchSeLinuxPolicies() {
|
offset += ret;
|
||||||
//
|
|
||||||
// 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 }\"");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// Takes a byte buffer to use to read the output into.
|
||||||
// This buffer MUST be in native byte order and at least
|
// This buffer MUST be in native byte order and at least
|
||||||
// EVDEV_MAX_EVENT_SIZE bytes long.
|
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||||
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
public static EvdevEvent read(InputStream input) throws IOException {
|
||||||
int bytesRead = read(fd, buffer.array());
|
ByteBuffer bb;
|
||||||
if (bytesRead < 0) {
|
int packetLength;
|
||||||
LimeLog.warning("Failed to read: "+bytesRead);
|
|
||||||
return null;
|
// Read the packet length
|
||||||
}
|
bb = ByteBuffer.allocate(4).order(ByteOrder.nativeOrder());
|
||||||
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
readAll(input, bb);
|
||||||
LimeLog.warning("Short read: "+bytesRead);
|
packetLength = bb.getInt();
|
||||||
|
|
||||||
|
if (packetLength < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||||
|
LimeLog.warning("Short read: "+packetLength);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.limit(bytesRead);
|
// Read the rest of the packet
|
||||||
buffer.rewind();
|
bb = ByteBuffer.allocate(packetLength).order(ByteOrder.nativeOrder());
|
||||||
|
readAll(input, bb);
|
||||||
|
|
||||||
// Throw away the time stamp
|
// Throw away the time stamp
|
||||||
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
if (packetLength == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||||
buffer.getLong();
|
bb.getLong();
|
||||||
buffer.getLong();
|
bb.getLong();
|
||||||
} else {
|
} else {
|
||||||
buffer.getInt();
|
bb.getInt();
|
||||||
buffer.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);
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,4 +10,23 @@ LOCAL_MODULE := evdev_reader
|
|||||||
LOCAL_SRC_FILES := evdev_reader.c
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
LOCAL_LDLIBS := -llog
|
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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <jni.h>
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
@ -8,34 +9,38 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
#define REL_X 0x00
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
|
#define REL_Y 0x01
|
||||||
const char *path;
|
#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
|
// This is a small executable that runs in a root shell. It reads input
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
// devices and writes the evdev output packets to stdout. This allows
|
||||||
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
// Moonlight to read input devices without having to muck with changing
|
||||||
}
|
// device permissions or modifying SELinux policy (which is prevented in
|
||||||
|
// Marshmallow anyway).
|
||||||
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
|
|
||||||
|
|
||||||
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
static int hasRelAxis(int fd, short axis) {
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
|
||||||
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
||||||
|
|
||||||
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
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);
|
return test_bit(axis, relBitmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
static int hasKey(int fd, short key) {
|
||||||
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) {
|
|
||||||
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
||||||
|
|
||||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
||||||
@ -61,24 +56,37 @@ Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject t
|
|||||||
return test_bit(key, keyBitmask);
|
return test_bit(key, keyBitmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
static void outputEvdevData(char *data, int dataSize) {
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
|
// We need to hold our StdoutLock to avoid garbling
|
||||||
jint ret;
|
// input data when multiple threads try to write at once.
|
||||||
jbyte *data;
|
pthread_mutex_lock(&StdoutLock);
|
||||||
int pollres;
|
fwrite(&dataSize, sizeof(dataSize), 1, stdout);
|
||||||
struct pollfd pollinfo;
|
fwrite(data, dataSize, 1, stdout);
|
||||||
|
fflush(stdout);
|
||||||
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
pthread_mutex_unlock(&StdoutLock);
|
||||||
if (data == NULL) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"Failed to get byte array");
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
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
|
// Unwait every 250 ms to return to caller if the fd is closed
|
||||||
pollinfo.fd = fd;
|
pollinfo.fd = device->fd;
|
||||||
pollinfo.events = POLLIN;
|
pollinfo.events = POLLIN;
|
||||||
pollinfo.revents = 0;
|
pollinfo.revents = 0;
|
||||||
pollres = poll(&pollinfo, 1, 250);
|
pollres = poll(&pollinfo, 1, 250);
|
||||||
@ -87,16 +95,23 @@ Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject thi
|
|||||||
|
|
||||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
// We'll have data available now
|
// We'll have data available now
|
||||||
ret = read(fd, data, sizeof(struct input_event));
|
ret = read(device->fd, data, sizeof(struct input_event));
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
"read() failed: %d", errno);
|
"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 {
|
else {
|
||||||
// There must have been a failure
|
|
||||||
ret = -1;
|
|
||||||
|
|
||||||
if (pollres < 0) {
|
if (pollres < 0) {
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
"poll() failed: %d", errno);
|
"poll() failed: %d", errno);
|
||||||
@ -105,14 +120,207 @@ Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject thi
|
|||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
"Unexpected revents: %d", pollinfo.revents);
|
"Unexpected revents: %d", pollinfo.revents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Terminate this thread
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
startPollForDevice(dirEnt->d_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(inputDir);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
// Wait for requests from the client
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
|
for (;;) {
|
||||||
return close(fd);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user