From f4546ba1884d075e0fe34586e3754111dba4fc18 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 1 Sep 2014 22:19:12 -0700 Subject: [PATCH] Raw mouse input WIP --- AndroidManifest.xml | 7 +- src/com/limelight/Game.java | 51 ++++++- src/com/limelight/LimelightBuildProps.java | 5 + .../binding/input/evdev/EvdevEvent.java | 30 ++++ .../binding/input/evdev/EvdevHandler.java | 128 ++++++++++++++++++ .../binding/input/evdev/EvdevListener.java | 10 ++ .../binding/input/evdev/EvdevReader.java | 55 ++++++++ .../binding/input/evdev/EvdevWatcher.java | 75 ++++++++++ 8 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 src/com/limelight/LimelightBuildProps.java create mode 100644 src/com/limelight/binding/input/evdev/EvdevEvent.java create mode 100644 src/com/limelight/binding/input/evdev/EvdevHandler.java create mode 100644 src/com/limelight/binding/input/evdev/EvdevListener.java create mode 100644 src/com/limelight/binding/input/evdev/EvdevReader.java create mode 100644 src/com/limelight/binding/input/evdev/EvdevWatcher.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c7fb64a9..5099e633 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@  + android:versionCode="32" + android:versionName="2.5.4" > + + + diff --git a/src/com/limelight/Game.java b/src/com/limelight/Game.java index b1bd2cbe..b661edf8 100644 --- a/src/com/limelight/Game.java +++ b/src/com/limelight/Game.java @@ -5,6 +5,8 @@ import com.limelight.binding.PlatformBinding; import com.limelight.binding.input.ControllerHandler; import com.limelight.binding.input.KeyboardTranslator; import com.limelight.binding.input.TouchContext; +import com.limelight.binding.input.evdev.EvdevListener; +import com.limelight.binding.input.evdev.EvdevWatcher; import com.limelight.binding.video.ConfigurableDecoderRenderer; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnectionListener; @@ -40,7 +42,9 @@ import android.view.WindowManager; import android.widget.Toast; -public class Game extends Activity implements SurfaceHolder.Callback, OnGenericMotionListener, OnTouchListener, NvConnectionListener { +public class Game extends Activity implements SurfaceHolder.Callback, + OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener +{ private int lastMouseX = Integer.MIN_VALUE; private int lastMouseY = Integer.MIN_VALUE; private int lastButtonState = 0; @@ -64,6 +68,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM private boolean stretchToFit; private boolean toastsDisabled; + private EvdevWatcher evdevWatcher; + private ConfigurableDecoderRenderer decoderRenderer; private WifiManager.WifiLock wifiLock; @@ -205,6 +211,12 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM touchContextMap[i] = new TouchContext(conn, i); } + if (LimelightBuildProps.ROOT_BUILD) { + // Start watching for raw input + evdevWatcher = new EvdevWatcher(this); + evdevWatcher.start(); + } + // The connection will be started when the surface gets created sh.addCallback(this); } @@ -288,6 +300,10 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM if (message != null) { Toast.makeText(this, message, Toast.LENGTH_LONG).show(); } + + if (LimelightBuildProps.ROOT_BUILD) { + evdevWatcher.shutdown(); + } finish(); } @@ -631,4 +647,37 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM connected = false; } } + + @Override + public void mouseMove(int deltaX, int deltaY) { + conn.sendMouseMove((short) deltaX, (short) deltaY); + } + + @Override + public void mouseButtonEvent(int buttonId, boolean down) { + byte buttonIndex; + + switch (buttonId) + { + case EvdevListener.BUTTON_LEFT: + buttonIndex = MouseButtonPacket.BUTTON_LEFT; + break; + case EvdevListener.BUTTON_MIDDLE: + buttonIndex = MouseButtonPacket.BUTTON_MIDDLE; + break; + case EvdevListener.BUTTON_RIGHT: + buttonIndex = MouseButtonPacket.BUTTON_RIGHT; + break; + default: + LimeLog.warning("Unhandled button: "+buttonId); + return; + } + + if (down) { + conn.sendMouseButtonDown(buttonIndex); + } + else { + conn.sendMouseButtonUp(buttonIndex); + } + } } diff --git a/src/com/limelight/LimelightBuildProps.java b/src/com/limelight/LimelightBuildProps.java new file mode 100644 index 00000000..c3934275 --- /dev/null +++ b/src/com/limelight/LimelightBuildProps.java @@ -0,0 +1,5 @@ +package com.limelight; + +public class LimelightBuildProps { + public static final boolean ROOT_BUILD = false; +} diff --git a/src/com/limelight/binding/input/evdev/EvdevEvent.java b/src/com/limelight/binding/input/evdev/EvdevEvent.java new file mode 100644 index 00000000..a01bedcd --- /dev/null +++ b/src/com/limelight/binding/input/evdev/EvdevEvent.java @@ -0,0 +1,30 @@ +package com.limelight.binding.input.evdev; + +public class EvdevEvent { + public static final int EVDEV_MIN_EVENT_SIZE = 18; + public static final int EVDEV_MAX_EVENT_SIZE = 24; + + /* Event types */ + public static final short EV_SYN = 0x00; + public static final short EV_KEY = 0x01; + public static final short EV_REL = 0x02; + + /* Relative axes */ + public static final short REL_X = 0x00; + public static final short REL_Y = 0x01; + + /* Buttons */ + public static final short BTN_LEFT = 0x110; + public static final short BTN_RIGHT = 0x111; + public static final short BTN_MIDDLE = 0x112; + + public short type; + public short code; + public int value; + + public EvdevEvent(short type, short code, int value) { + this.type = type; + this.code = code; + this.value = value; + } +} diff --git a/src/com/limelight/binding/input/evdev/EvdevHandler.java b/src/com/limelight/binding/input/evdev/EvdevHandler.java new file mode 100644 index 00000000..65fde688 --- /dev/null +++ b/src/com/limelight/binding/input/evdev/EvdevHandler.java @@ -0,0 +1,128 @@ +package com.limelight.binding.input.evdev; + +import java.nio.ByteBuffer; + +import com.limelight.LimeLog; + +public class EvdevHandler { + + private String absolutePath; + private EvdevListener listener; + private boolean shutdown = false; + + private 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. + + // Modify permissions to allow us access + if (!EvdevReader.setPermissions(absolutePath, 0666)) { + LimeLog.warning("Unable to chmod "+absolutePath); + return; + } + + try { + // Open the /dev/input/eventX file + int fd = EvdevReader.open(absolutePath); + if (fd == -1) { + LimeLog.warning("Unable to open "+absolutePath); + return; + } + + try { + // Check if it's a mouse + if (!EvdevReader.isMouse(fd)) { + // We only handle mice + return; + } + + // Grab it for ourselves + if (!EvdevReader.grab(fd)) { + LimeLog.warning("Unable to grab "+absolutePath); + return; + } + + ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE); + + try { + while (!isInterrupted() && !shutdown) { + EvdevEvent event = EvdevReader.read(fd, buffer); + if (event == null) { + return; + } + + switch (event.type) + { + case EvdevEvent.EV_SYN: + // Do nothing + break; + + case EvdevEvent.EV_REL: + switch (event.code) + { + case EvdevEvent.REL_X: + listener.mouseMove(event.value, 0); + break; + case EvdevEvent.REL_Y: + listener.mouseMove(0, event.value); + break; + } + break; + + case EvdevEvent.EV_KEY: + switch (event.code) + { + case EvdevEvent.BTN_LEFT: + listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, + event.value != 0); + break; + case EvdevEvent.BTN_MIDDLE: + listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE, + event.value != 0); + break; + case EvdevEvent.BTN_RIGHT: + listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT, + event.value != 0); + break; + } + } + } + } finally { + // Release our grab + EvdevReader.ungrab(fd); + } + } finally { + // Close the file + EvdevReader.close(fd); + } + } finally { + // Set permissions back + EvdevReader.setPermissions(absolutePath, 0066); + } + } + }; + + public EvdevHandler(String absolutePath, EvdevListener listener) { + this.absolutePath = absolutePath; + this.listener = listener; + } + + public void start() { + handlerThread.start(); + } + + public void stop() { + shutdown = true; + handlerThread.interrupt(); + + try { + handlerThread.join(); + } catch (InterruptedException e) {} + } + + public void notifyDeleted() { + stop(); + } +} diff --git a/src/com/limelight/binding/input/evdev/EvdevListener.java b/src/com/limelight/binding/input/evdev/EvdevListener.java new file mode 100644 index 00000000..a8f781ea --- /dev/null +++ b/src/com/limelight/binding/input/evdev/EvdevListener.java @@ -0,0 +1,10 @@ +package com.limelight.binding.input.evdev; + +public interface EvdevListener { + public static final int BUTTON_LEFT = 1; + public static final int BUTTON_MIDDLE = 2; + public static final int BUTTON_RIGHT = 3; + + public void mouseMove(int deltaX, int deltaY); + public void mouseButtonEvent(int buttonId, boolean down); +} diff --git a/src/com/limelight/binding/input/evdev/EvdevReader.java b/src/com/limelight/binding/input/evdev/EvdevReader.java new file mode 100644 index 00000000..9687865b --- /dev/null +++ b/src/com/limelight/binding/input/evdev/EvdevReader.java @@ -0,0 +1,55 @@ +package com.limelight.binding.input.evdev; + +import java.nio.ByteBuffer; + +import com.limelight.LimeLog; + +public class EvdevReader { + // Requires root to chmod /dev/input/eventX + public static native boolean setPermissions(String fileName, int octalPermissions); + + // 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); + + // Returns true if the device is a mouse + public static native boolean isMouse(int fd); + + // 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); + return null; + } + + buffer.limit(bytesRead); + buffer.rewind(); + + // Throw away the time stamp + if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { + buffer.getLong(); + buffer.getLong(); + } else { + buffer.getInt(); + buffer.getInt(); + } + + return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt()); + } + + // Closes the fd from open() + public static native int close(int fd); +} diff --git a/src/com/limelight/binding/input/evdev/EvdevWatcher.java b/src/com/limelight/binding/input/evdev/EvdevWatcher.java new file mode 100644 index 00000000..3f266804 --- /dev/null +++ b/src/com/limelight/binding/input/evdev/EvdevWatcher.java @@ -0,0 +1,75 @@ +package com.limelight.binding.input.evdev; + +import java.io.File; +import java.util.HashMap; + +import android.os.FileObserver; + +public class EvdevWatcher { + private static final String PATH = "/dev/input"; + private static final String REQUIRED_FILE_PREFIX = "event"; + + private HashMap handlers = new HashMap(); + private boolean shutdown = false; + private EvdevListener listener; + + private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { + @Override + public void onEvent(int event, String fileName) { + if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { + return; + } + + synchronized (handlers) { + if (shutdown) { + return; + } + + if ((event & FileObserver.CREATE) != 0) { + EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); + handler.start(); + + handlers.put(fileName, handler); + } + + if ((event & FileObserver.DELETE) != 0) { + EvdevHandler handler = handlers.get(fileName); + if (handler != null) { + handler.notifyDeleted(); + } + } + } + } + }; + + public EvdevWatcher(EvdevListener listener) { + this.listener = listener; + } + + public void start() { + // Start watching for new files + observer.startWatching(); + + // Rundown existing files and generate synthetic events + File devInputDir = new File(PATH); + File[] files = devInputDir.listFiles(); + for (File f : files) { + observer.onEvent(FileObserver.CREATE, f.getName()); + } + } + + public void shutdown() { + // Stop the observer + observer.stopWatching(); + + synchronized (handlers) { + // Stop creating new handlers + shutdown = true; + + // Stop all handlers + for (EvdevHandler handler : handlers.values()) { + handler.stop(); + } + } + } +}