From fe3b649fe97afbb1a15b038df58a1c4aacaebc23 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 31 Oct 2015 17:07:55 -0700 Subject: [PATCH 1/4] Bump version to 3.1.12 --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 296e8754..4ac15fb8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,14 +5,14 @@ apply plugin: 'com.android.application' android { compileSdkVersion 23 - buildToolsVersion "23.0.1" + buildToolsVersion "23.0.2" defaultConfig { minSdkVersion 16 targetSdkVersion 23 - versionName "3.1.11" - versionCode = 66 + versionName "3.1.12" + versionCode = 69 } productFlavors { From d740e7a5216af99cfc0bfdcb1787866700303568 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 8 Nov 2015 16:12:18 -0800 Subject: [PATCH 2/4] Add an Xbox One controller driver developed based on the xpad driver in the Linux kernel --- app/src/main/AndroidManifest.xml | 3 + app/src/main/java/com/limelight/Game.java | 41 +++- .../binding/input/ControllerHandler.java | 215 +++++++++++----- .../input/driver/UsbDriverListener.java | 11 + .../input/driver/UsbDriverService.java | 135 ++++++++++ .../input/driver/XboxOneController.java | 231 ++++++++++++++++++ .../preferences/PreferenceConfiguration.java | 5 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 5 + 9 files changed, 581 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/com/limelight/binding/input/driver/UsbDriverListener.java create mode 100644 app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java create mode 100644 app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java 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" /> + Date: Sun, 8 Nov 2015 19:03:12 -0800 Subject: [PATCH 3/4] Fix some listener bugs in the XB1 driver --- .../input/driver/UsbDriverService.java | 63 ++++++++++++++----- .../input/driver/XboxOneController.java | 32 +++++----- 2 files changed, 63 insertions(+), 32 deletions(-) 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 index 92091bee..fdafbcc0 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java +++ b/app/src/main/java/com/limelight/binding/input/driver/UsbDriverService.java @@ -14,7 +14,7 @@ import android.os.IBinder; import java.util.ArrayList; -public class UsbDriverService extends Service { +public class UsbDriverService extends Service implements UsbDriverListener { private static final String ACTION_USB_PERMISSION = "com.limelight.USB_PERMISSION"; @@ -29,6 +29,38 @@ public class UsbDriverService extends Service { private UsbDriverListener listener; private static int nextDeviceId; + @Override + public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) { + // Call through to the client's listener + if (listener != null) { + listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger); + } + } + + @Override + public void deviceRemoved(int controllerId) { + // Remove the the controller from our list (if not removed already) + for (XboxOneController controller : controllers) { + if (controller.getControllerId() == controllerId) { + controllers.remove(controller); + break; + } + } + + // Call through to the client's listener + if (listener != null) { + listener.deviceRemoved(controllerId); + } + } + + @Override + public void deviceAdded(int controllerId) { + // Call through to the client's listener + if (listener != null) { + listener.deviceAdded(controllerId); + } + } + public class UsbEventReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -56,13 +88,13 @@ public class UsbDriverService extends Service { 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); + // Report all controllerMap that already exist + if (listener != null) { + for (XboxOneController controller : controllers) { + listener.deviceAdded(controller.getControllerId()); + } + } } } @@ -80,23 +112,20 @@ public class UsbDriverService extends Service { UsbDeviceConnection connection = usbManager.openDevice(device); // Try to initialize it - XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++); + XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++, this); if (!controller.start()) { connection.close(); return; } - // Add to the list + // Add this controller to the list controllers.add(controller); - - // Add listener - updateListeners(); } } @Override public void onCreate() { - usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); // Register for USB attach broadcasts and permission completions IntentFilter filter = new IntentFilter(); @@ -118,13 +147,13 @@ public class UsbDriverService extends Service { // Stop the attachment receiver unregisterReceiver(receiver); - // Remove all listeners + // Remove listeners listener = null; - updateListeners(); // Stop all controllers - for (XboxOneController controller : controllers) { - controller.stop(); + while (controllers.size() > 0) { + // Stop and remove the controller + controllers.remove(0).stop(); } } 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 index 79072792..acbcabb5 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java +++ b/app/src/main/java/com/limelight/binding/input/driver/XboxOneController.java @@ -5,7 +5,6 @@ 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; @@ -35,18 +34,15 @@ public class XboxOneController { // FIXME: odata_serial private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00}; - public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId) { + public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) { this.device = device; this.connection = connection; this.deviceId = deviceId; + this.listener = listener; } - public void setListener(UsbDriverListener listener) { - this.listener = listener; - - if (listener != null) { - listener.deviceAdded(deviceId); - } + public int getControllerId() { + return this.deviceId; } private void setButtonFlag(int buttonFlag, int data) { @@ -59,10 +55,8 @@ public class XboxOneController { } private void reportInput() { - if (listener != null) { - listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY, - rightStickX, rightStickY, leftTrigger, rightTrigger); - } + listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY, + rightStickX, rightStickY, leftTrigger, rightTrigger); } private void processButtons(ByteBuffer buffer) { @@ -203,21 +197,29 @@ public class XboxOneController { // Start listening for controller input startInputThread(inEndpt); + // Report this device added via the listener + listener.deviceAdded(deviceId); + return true; } public void stop() { + if (stopped) { + return; + } + stopped = true; + // Stop the input thread if (inputThread != null) { inputThread.interrupt(); inputThread = null; } - if (listener != null) { - listener.deviceRemoved(deviceId); - } + // Report the device removed + listener.deviceRemoved(deviceId); + // Close the USB connection connection.close(); } From e89e803d54430795d3620c1e6048cbd5891632a1 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 8 Nov 2015 19:05:22 -0800 Subject: [PATCH 4/4] Zero controller values before removing a controller --- .../com/limelight/binding/input/ControllerHandler.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 d8ceca0c..695468c8 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -120,6 +120,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD } private void releaseControllerNumber(GenericControllerContext context) { + // If this device sent data as a gamepad, zero the values before removing + if (context.assignedControllerNumber) { + conn.sendControllerInput(context.controllerNumber, (short) 0, + (byte) 0, (byte) 0, + (short) 0, (short) 0, + (short) 0, (short) 0); + } + + // If we reserved a controller number, remove that reservation if (context.reservedControllerNumber) { LimeLog.info("Controller number "+context.controllerNumber+" is now available"); currentControllers &= ~(1 << context.controllerNumber);