diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a0076d6c..a826a69a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -81,6 +81,9 @@
+
diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java
index 9ec628b8..e4e6e2ba 100644
--- a/app/src/main/java/com/limelight/Game.java
+++ b/app/src/main/java/com/limelight/Game.java
@@ -5,6 +5,7 @@ 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.driver.UsbDriverService;
import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.evdev.EvdevWatcher;
import com.limelight.binding.video.ConfigurableDecoderRenderer;
@@ -22,7 +23,11 @@ import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint;
import android.app.Activity;
+import android.app.Service;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.input.InputManager;
@@ -31,6 +36,7 @@ import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
import android.os.SystemClock;
import android.view.Display;
import android.view.InputDevice;
@@ -92,6 +98,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int drFlags = 0;
+ private boolean connectedToUsbDriverService = false;
+ private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ UsbDriverService.UsbDriverBinder binder = (UsbDriverService.UsbDriverBinder) iBinder;
+ binder.setListener(controllerHandler);
+ connectedToUsbDriverService = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ connectedToUsbDriverService = false;
+ }
+ };
+
public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId";
@@ -248,6 +269,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
evdevWatcher.start();
}
+ if (prefConfig.usbDriver) {
+ // Start the USB driver
+ bindService(new Intent(this, UsbDriverService.class),
+ usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
+ }
+
// The connection will be started when the surface gets created
sh.addCallback(this);
}
@@ -315,6 +342,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
+ wifiLock.release();
+
+ if (connectedToUsbDriverService) {
+ // Unbind from the discovery service
+ unbindService(usbDriverServiceConnection);
+ }
+
displayedFailureDialog = true;
stopConnection();
@@ -338,13 +372,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
finish();
}
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- wifiLock.release();
- }
-
private final Runnable toggleGrab = new Runnable() {
@Override
public void run() {
diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java
index 0613b294..d8ceca0c 100644
--- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java
+++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java
@@ -8,12 +8,13 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import com.limelight.LimeLog;
+import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
-public class ControllerHandler implements InputManager.InputDeviceListener {
+public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
@@ -29,11 +30,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
private final Vector2d inputVector = new Vector2d();
- private final SparseArray contexts = new SparseArray();
+ private final SparseArray inputDeviceContexts = new SparseArray<>();
+ private final SparseArray usbDeviceContexts = new SparseArray<>();
private final NvConnection conn;
private final double stickDeadzone;
- private final ControllerContext defaultContext = new ControllerContext();
+ private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
private boolean hasGameController;
@@ -102,11 +104,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
@Override
public void onInputDeviceRemoved(int deviceId) {
- ControllerContext context = contexts.get(deviceId);
+ InputDeviceContext context = inputDeviceContexts.get(deviceId);
if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context);
- contexts.remove(deviceId);
+ inputDeviceContexts.remove(deviceId);
}
}
@@ -117,7 +119,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
onInputDeviceAdded(deviceId);
}
- private void releaseControllerNumber(ControllerContext context) {
+ private void releaseControllerNumber(GenericControllerContext context) {
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
@@ -126,42 +128,78 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
// Called before sending input but after we've determined that this
// is definitely a controller (not a keyboard, mouse, or something else)
- private void assignControllerNumberIfNeeded(ControllerContext context) {
+ private void assignControllerNumberIfNeeded(GenericControllerContext context) {
if (context.assignedControllerNumber) {
return;
}
- LimeLog.info(context.name+" ("+context.id+") needs a controller number assigned");
- if (context.name != null && context.name.contains("gpio-keys")) {
- // This is the back button on Shield portable consoles
- LimeLog.info("Built-in buttons hardcoded as controller 0");
- context.controllerNumber = 0;
- }
- else if (multiControllerEnabled && context.hasJoystickAxes) {
- context.controllerNumber = 0;
+ if (context instanceof InputDeviceContext) {
+ InputDeviceContext devContext = (InputDeviceContext) context;
- LimeLog.info("Reserving the next available controller number");
- for (short i = 0; i < 4; i++) {
- if ((currentControllers & (1 << i)) == 0) {
- // Found an unused controller value
- currentControllers |= (1 << i);
- context.controllerNumber = i;
- context.reservedControllerNumber = true;
- break;
+ LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
+ if (devContext.name != null && devContext.name.contains("gpio-keys")) {
+ // This is the back button on Shield portable consoles
+ LimeLog.info("Built-in buttons hardcoded as controller 0");
+ context.controllerNumber = 0;
+ }
+ else if (multiControllerEnabled && devContext.hasJoystickAxes) {
+ context.controllerNumber = 0;
+
+ LimeLog.info("Reserving the next available controller number");
+ for (short i = 0; i < 4; i++) {
+ if ((currentControllers & (1 << i)) == 0) {
+ // Found an unused controller value
+ currentControllers |= (1 << i);
+ context.controllerNumber = i;
+ context.reservedControllerNumber = true;
+ break;
+ }
}
}
+ else {
+ LimeLog.info("Not reserving a controller number");
+ context.controllerNumber = 0;
+ }
}
else {
- LimeLog.info("Not reserving a controller number");
- context.controllerNumber = 0;
+ if (multiControllerEnabled) {
+ context.controllerNumber = 0;
+
+ LimeLog.info("Reserving the next available controller number");
+ for (short i = 0; i < 4; i++) {
+ if ((currentControllers & (1 << i)) == 0) {
+ // Found an unused controller value
+ currentControllers |= (1 << i);
+ context.controllerNumber = i;
+ context.reservedControllerNumber = true;
+ break;
+ }
+ }
+ }
+ else {
+ LimeLog.info("Not reserving a controller number");
+ context.controllerNumber = 0;
+ }
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
context.assignedControllerNumber = true;
}
- private ControllerContext createContextForDevice(InputDevice dev) {
- ControllerContext context = new ControllerContext();
+ private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
+ UsbDeviceContext context = new UsbDeviceContext();
+
+ context.id = deviceId;
+
+ context.leftStickDeadzoneRadius = (float) stickDeadzone;
+ context.rightStickDeadzoneRadius = (float) stickDeadzone;
+ context.triggerDeadzone = 0.13f;
+
+ return context;
+ }
+
+ private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
+ InputDeviceContext context = new InputDeviceContext();
String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName);
@@ -332,26 +370,26 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
return context;
}
- private ControllerContext getContextForDevice(InputDevice dev) {
+ private InputDeviceContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default context
if (dev == null) {
return defaultContext;
}
// Return the existing context if it exists
- ControllerContext context = contexts.get(dev.getId());
+ InputDeviceContext context = inputDeviceContexts.get(dev.getId());
if (context != null) {
return context;
}
// Otherwise create a new context
- context = createContextForDevice(dev);
- contexts.put(dev.getId(), context);
+ context = createInputDeviceContextForDevice(dev);
+ inputDeviceContexts.put(dev.getId(), context);
return context;
}
- private void sendControllerInputPacket(ControllerContext context) {
+ private void sendControllerInputPacket(GenericControllerContext context) {
assignControllerNumberIfNeeded(context);
conn.sendControllerInput(context.controllerNumber, context.inputMap,
context.leftTrigger, context.rightTrigger,
@@ -361,7 +399,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
// Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL
- private int handleRemapping(ControllerContext context, KeyEvent event) {
+ private int handleRemapping(InputDeviceContext context, KeyEvent event) {
// Don't capture the back button if configured
if (context.ignoreBack) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
@@ -499,7 +537,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
// evaluates the deadzone.
}
- private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
+ private void handleAxisSet(InputDeviceContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) {
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
@@ -559,7 +597,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
public boolean handleMotionEvent(MotionEvent event) {
- ControllerContext context = getContextForDevice(event.getDevice());
+ InputDeviceContext context = getContextForDevice(event.getDevice());
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// We purposefully ignore the historical values in the motion event as it makes
@@ -591,7 +629,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
public boolean handleButtonUp(KeyEvent event) {
- ControllerContext context = getContextForDevice(event.getDevice());
+ InputDeviceContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
@@ -716,7 +754,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
public boolean handleButtonDown(KeyEvent event) {
- ControllerContext context = getContextForDevice(event.getDevice());
+ InputDeviceContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
@@ -816,34 +854,65 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
return true;
}
- class ControllerContext {
- public String name;
+ @Override
+ public void reportControllerState(int controllerId, short buttonFlags,
+ float leftStickX, float leftStickY,
+ float rightStickX, float rightStickY,
+ float leftTrigger, float rightTrigger) {
+ UsbDeviceContext context = usbDeviceContexts.get(controllerId);
+
+ Vector2d leftStickVector = populateCachedVector(leftStickX, leftStickY);
+
+ handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
+
+ context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
+ context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
+
+ Vector2d rightStickVector = populateCachedVector(rightStickX, rightStickY);
+
+ handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
+
+ context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
+ context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
+
+ if (leftTrigger <= context.triggerDeadzone) {
+ leftTrigger = 0;
+ }
+ if (rightTrigger <= context.triggerDeadzone) {
+ rightTrigger = 0;
+ }
+
+ context.leftTrigger = (byte)(leftTrigger * 0xFF);
+ context.rightTrigger = (byte)(rightTrigger * 0xFF);
+
+ context.inputMap = buttonFlags;
+
+ sendControllerInputPacket(context);
+ }
+
+ @Override
+ public void deviceRemoved(int controllerId) {
+ UsbDeviceContext context = usbDeviceContexts.get(controllerId);
+ if (context != null) {
+ LimeLog.info("Removed controller: "+controllerId);
+ releaseControllerNumber(context);
+ usbDeviceContexts.remove(controllerId);
+ }
+ }
+
+ @Override
+ public void deviceAdded(int controllerId) {
+ UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
+ usbDeviceContexts.put(controllerId, context);
+ }
+
+ class GenericControllerContext {
public int id;
- public int leftStickXAxis = -1;
- public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius;
-
- public int rightStickXAxis = -1;
- public int rightStickYAxis = -1;
public float rightStickDeadzoneRadius;
-
- public int leftTriggerAxis = -1;
- public int rightTriggerAxis = -1;
- public boolean triggersIdleNegative;
public float triggerDeadzone;
- public int hatXAxis = -1;
- public int hatYAxis = -1;
-
- public boolean isDualShock4;
- public boolean isXboxController;
- public boolean isServal;
- public boolean backIsStart;
- public boolean modeIsSelect;
- public boolean ignoreBack;
- public boolean hasJoystickAxes;
-
public boolean assignedControllerNumber;
public boolean reservedControllerNumber;
public short controllerNumber;
@@ -855,6 +924,32 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
+ }
+
+ class InputDeviceContext extends GenericControllerContext {
+ public String name;
+
+ public int leftStickXAxis = -1;
+ public int leftStickYAxis = -1;
+
+ public int rightStickXAxis = -1;
+ public int rightStickYAxis = -1;
+
+ public int leftTriggerAxis = -1;
+ public int rightTriggerAxis = -1;
+ public boolean triggersIdleNegative;
+
+ public int hatXAxis = -1;
+ public int hatYAxis = -1;
+
+ public boolean isDualShock4;
+ public boolean isXboxController;
+ public boolean isServal;
+ public boolean backIsStart;
+ public boolean modeIsSelect;
+ public boolean ignoreBack;
+ public boolean hasJoystickAxes;
+
public int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
@@ -867,4 +962,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
public long startDownTime = 0;
}
+
+ class UsbDeviceContext extends GenericControllerContext {}
}
diff --git a/app/src/main/java/com/limelight/binding/input/driver/UsbDriverListener.java b/app/src/main/java/com/limelight/binding/input/driver/UsbDriverListener.java
new file mode 100644
index 00000000..45d07c2d
--- /dev/null
+++ b/app/src/main/java/com/limelight/binding/input/driver/UsbDriverListener.java
@@ -0,0 +1,11 @@
+package com.limelight.binding.input.driver;
+
+public interface UsbDriverListener {
+ void reportControllerState(int controllerId, short buttonFlags,
+ float leftStickX, float leftStickY,
+ float rightStickX, float rightStickY,
+ float leftTrigger, float rightTrigger);
+
+ void deviceRemoved(int controllerId);
+ void deviceAdded(int controllerId);
+}
diff --git a/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java b/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java
new file mode 100644
index 00000000..92091bee
--- /dev/null
+++ b/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java
@@ -0,0 +1,135 @@
+package com.limelight.binding.input.driver;
+
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbManager;
+import android.os.Binder;
+import android.os.IBinder;
+
+import java.util.ArrayList;
+
+public class UsbDriverService extends Service {
+
+ private static final String ACTION_USB_PERMISSION =
+ "com.limelight.USB_PERMISSION";
+
+ private UsbManager usbManager;
+
+ private final UsbEventReceiver receiver = new UsbEventReceiver();
+ private final UsbDriverBinder binder = new UsbDriverBinder();
+
+ private final ArrayList controllers = new ArrayList<>();
+
+ private UsbDriverListener listener;
+ private static int nextDeviceId;
+
+ public class UsbEventReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ // Initial attachment broadcast
+ if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
+ UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+
+ // Continue the state machine
+ handleUsbDeviceState(device);
+ }
+ // Subsequent permission dialog completion intent
+ else if (action.equals(ACTION_USB_PERMISSION)) {
+ UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+
+ // If we got this far, we've already found we're able to handle this device
+ if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
+ handleUsbDeviceState(device);
+ }
+ }
+ }
+ }
+
+ public class UsbDriverBinder extends Binder {
+ public void setListener(UsbDriverListener listener) {
+ UsbDriverService.this.listener = listener;
+ updateListeners();
+ }
+ }
+
+ private void updateListeners() {
+ for (XboxOneController controller : controllers) {
+ controller.setListener(listener);
+ }
+ }
+
+ private void handleUsbDeviceState(UsbDevice device) {
+ // Are we able to operate it?
+ if (XboxOneController.canClaimDevice(device)) {
+ // Do we have permission yet?
+ if (!usbManager.hasPermission(device)) {
+ // Let's ask for permission
+ usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
+ return;
+ }
+
+ // Open the device
+ UsbDeviceConnection connection = usbManager.openDevice(device);
+
+ // Try to initialize it
+ XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++);
+ if (!controller.start()) {
+ connection.close();
+ return;
+ }
+
+ // Add to the list
+ controllers.add(controller);
+
+ // Add listener
+ updateListeners();
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
+
+ // Register for USB attach broadcasts and permission completions
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ filter.addAction(ACTION_USB_PERMISSION);
+ registerReceiver(receiver, filter);
+
+ // Enumerate existing devices
+ for (UsbDevice dev : usbManager.getDeviceList().values()) {
+ if (XboxOneController.canClaimDevice(dev)) {
+ // Start the process of claiming this device
+ handleUsbDeviceState(dev);
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ // Stop the attachment receiver
+ unregisterReceiver(receiver);
+
+ // Remove all listeners
+ listener = null;
+ updateListeners();
+
+ // Stop all controllers
+ for (XboxOneController controller : controllers) {
+ controller.stop();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+}
diff --git a/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java b/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java
new file mode 100644
index 00000000..79072792
--- /dev/null
+++ b/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java
@@ -0,0 +1,231 @@
+package com.limelight.binding.input.driver;
+
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.media.MediaCodec;
+
+import com.limelight.LimeLog;
+import com.limelight.binding.video.MediaCodecHelper;
+import com.limelight.nvstream.input.ControllerPacket;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class XboxOneController {
+ private final UsbDevice device;
+ private final UsbDeviceConnection connection;
+ private final int deviceId;
+
+ private Thread inputThread;
+ private UsbDriverListener listener;
+ private boolean stopped;
+
+ private short buttonFlags;
+ private float leftTrigger, rightTrigger;
+ private float rightStickX, rightStickY;
+ private float leftStickX, leftStickY;
+
+ private static final int MICROSOFT_VID = 0x045e;
+ private static final int XB1_IFACE_SUBCLASS = 71;
+ private static final int XB1_IFACE_PROTOCOL = 208;
+
+ // FIXME: odata_serial
+ private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
+
+ public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId) {
+ this.device = device;
+ this.connection = connection;
+ this.deviceId = deviceId;
+ }
+
+ public void setListener(UsbDriverListener listener) {
+ this.listener = listener;
+
+ if (listener != null) {
+ listener.deviceAdded(deviceId);
+ }
+ }
+
+ private void setButtonFlag(int buttonFlag, int data) {
+ if (data != 0) {
+ buttonFlags |= buttonFlag;
+ }
+ else {
+ buttonFlags &= ~buttonFlag;
+ }
+ }
+
+ private void reportInput() {
+ if (listener != null) {
+ listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
+ rightStickX, rightStickY, leftTrigger, rightTrigger);
+ }
+ }
+
+ private void processButtons(ByteBuffer buffer) {
+ byte b = buffer.get();
+
+ setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
+ setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
+
+ setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
+ setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
+ setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
+ setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
+
+ b = buffer.get();
+ setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
+ setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
+ setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
+ setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
+
+ setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
+ setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
+
+ setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
+ setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
+
+ leftTrigger = buffer.getShort() / 1023.0f;
+ rightTrigger = buffer.getShort() / 1023.0f;
+
+ leftStickX = buffer.getShort() / 32767.0f;
+ leftStickY = ~buffer.getShort() / 32767.0f;
+
+ rightStickX = buffer.getShort() / 32767.0f;
+ rightStickY = ~buffer.getShort() / 32767.0f;
+
+ reportInput();
+ }
+
+ private void processPacket(ByteBuffer buffer) {
+ switch (buffer.get())
+ {
+ case 0x20:
+ buffer.position(buffer.position()+3);
+ processButtons(buffer);
+ break;
+
+ case 0x07:
+ buffer.position(buffer.position() + 3);
+ setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
+ reportInput();
+ break;
+ }
+ }
+
+ private void startInputThread(final UsbEndpoint inEndpt) {
+ inputThread = new Thread() {
+ public void run() {
+ while (!isInterrupted() && !stopped) {
+ byte[] buffer = new byte[64];
+
+ int res;
+
+ //
+ // There's no way that I can tell to determine if a device has failed
+ // or if the timeout has simply expired. We'll check how long the transfer
+ // took to fail and assume the device failed if it happened before the timeout
+ // expired.
+ //
+
+ do {
+ // Read the next input state packet
+ long lastMillis = MediaCodecHelper.getMonotonicMillis();
+ res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
+ if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
+ LimeLog.warning("Detected device I/O error");
+ XboxOneController.this.stop();
+ break;
+ }
+ } while (res == -1 && !isInterrupted() && !stopped);
+
+ if (res == -1 || stopped) {
+ break;
+ }
+
+ processPacket(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN));
+ }
+ }
+ };
+ inputThread.setName("Xbox One Controller - Input Thread");
+ inputThread.start();
+ }
+
+ public boolean start() {
+ // Force claim all interfaces
+ for (int i = 0; i < device.getInterfaceCount(); i++) {
+ UsbInterface iface = device.getInterface(i);
+
+ if (!connection.claimInterface(iface, true)) {
+ LimeLog.warning("Failed to claim interfaces");
+ return false;
+ }
+ }
+
+ // Find the endpoints
+ UsbEndpoint outEndpt = null;
+ UsbEndpoint inEndpt = null;
+ UsbInterface iface = device.getInterface(0);
+ for (int i = 0; i < iface.getEndpointCount(); i++) {
+ UsbEndpoint endpt = iface.getEndpoint(i);
+ if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
+ if (inEndpt != null) {
+ LimeLog.warning("Found duplicate IN endpoint");
+ return false;
+ }
+ inEndpt = endpt;
+ }
+ else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
+ if (outEndpt != null) {
+ LimeLog.warning("Found duplicate OUT endpoint");
+ return false;
+ }
+ outEndpt = endpt;
+ }
+ }
+
+ // Make sure the required endpoints were present
+ if (inEndpt == null || outEndpt == null) {
+ LimeLog.warning("Missing required endpoint");
+ return false;
+ }
+
+ // Send the initialization packet
+ int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
+ if (res != XB1_INIT_DATA.length) {
+ LimeLog.warning("Initialization transfer failed: "+res);
+ return false;
+ }
+
+ // Start listening for controller input
+ startInputThread(inEndpt);
+
+ return true;
+ }
+
+ public void stop() {
+ stopped = true;
+
+ if (inputThread != null) {
+ inputThread.interrupt();
+ inputThread = null;
+ }
+
+ if (listener != null) {
+ listener.deviceRemoved(deviceId);
+ }
+
+ connection.close();
+ }
+
+ public static boolean canClaimDevice(UsbDevice device) {
+ return device.getVendorId() == MICROSOFT_VID &&
+ device.getInterfaceCount() >= 1 &&
+ device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
+ device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
+ device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
+ }
+}
diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java
index 18dcc95b..e0b9b5ff 100644
--- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java
+++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java
@@ -19,6 +19,7 @@ public class PreferenceConfiguration {
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
+ private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -36,6 +37,7 @@ public class PreferenceConfiguration {
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
+ private static final boolean DEFAULT_USB_DRIVER = true;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
@@ -47,7 +49,7 @@ public class PreferenceConfiguration {
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
- public boolean listMode, smallIconMode, multiController;
+ public boolean listMode, smallIconMode, multiController, usbDriver;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
@@ -159,6 +161,7 @@ public class PreferenceConfiguration {
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
+ config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
return config;
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf6935d1..8690cd3b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -99,6 +99,8 @@
When unchecked, all controllers appear as one
Adjust analog stick deadzone
%
+ Xbox One controller driver
+ Enables a built-in USB driver for devices without native Xbox One controller support.
UI Settings
Language
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 678d0b7e..a4bf1eb3 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -38,6 +38,11 @@
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
+