diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index a3dd5716..844745dd 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -294,6 +294,16 @@ public class Game extends Activity implements SurfaceHolder.Callback, if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) { Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show(); } + + int gamepadMask = ControllerHandler.getAttachedControllerMask(this); + if (!prefConfig.multiController && gamepadMask != 0) { + // If any gamepads are present in non-MC mode, set only gamepad 1. + gamepadMask = 1; + } + if (prefConfig.onscreenController) { + // If we're using OSC, always set at least gamepad 1. + gamepadMask |= 1; + } StreamConfiguration config = new StreamConfiguration.Builder() .setResolution(prefConfig.width, prefConfig.height) @@ -307,6 +317,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, .setHevcBitratePercentageMultiplier(75) .setHevcSupported(decoderRenderer.isHevcSupported()) .setEnableHdr(willStreamHdr) + .setAttachedGamepadMask(gamepadMask) .setAudioConfiguration(prefConfig.enable51Surround ? MoonBridge.AUDIO_CONFIGURATION_51_SURROUND : MoonBridge.AUDIO_CONFIGURATION_STEREO) 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 3c04bf72..86d28bfc 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -2,6 +2,8 @@ package com.limelight.binding.input; import android.content.Context; import android.hardware.input.InputManager; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; import android.os.SystemClock; import android.util.SparseArray; import android.view.InputDevice; @@ -12,6 +14,7 @@ import android.widget.Toast; import com.limelight.LimeLog; import com.limelight.binding.input.driver.UsbDriverListener; +import com.limelight.binding.input.driver.UsbDriverService; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.input.ControllerPacket; import com.limelight.nvstream.input.MouseButtonPacket; @@ -48,7 +51,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD private boolean hasGameController; private final boolean multiControllerEnabled; - private short currentControllers; + private short currentControllers, initialControllers; public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) { this.activityContext = activityContext; @@ -102,6 +105,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD // consume these. Instead, let's ignore them since that's probably the // most likely case. defaultContext.ignoreBack = true; + + // Get the initially attached set of gamepads. As each gamepad receives + // its initial InputEvent, we will move these from this set onto the + // currentControllers set which will allow them to properly unplug + // if they are removed. + initialControllers = getAttachedControllerMask(activityContext); } private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) { @@ -139,8 +148,47 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD onInputDeviceAdded(deviceId); } + public static short getAttachedControllerMask(Context context) { + int count = 0; + short mask = 0; + + // Count all input devices that are gamepads + InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE); + for (int id : im.getInputDeviceIds()) { + InputDevice dev = im.getInputDevice(id); + if (dev == null) { + continue; + } + + if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0) { + LimeLog.info("Counting InputDevice: "+dev.getName()); + mask |= 1 << count++; + } + } + + // Count all USB devices that match our drivers + UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + for (UsbDevice dev : usbManager.getDeviceList().values()) { + if (UsbDriverService.shouldClaimDevice(dev)) { + LimeLog.info("Counting UsbDevice: "+dev.getDeviceName()); + mask |= 1 << count++; + } + } + + LimeLog.info("Enumerated "+count+" gamepads"); + return mask; + } + private void releaseControllerNumber(GenericControllerContext context) { - // If this device sent data as a gamepad, zero the values before removing + // 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); + } + + // If this device sent data as a gamepad, zero the values before removing. + // We must do this after clearing the currentControllers entry so this + // causes the device to be removed on the server PC. if (context.assignedControllerNumber) { conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(), (short) 0, @@ -148,12 +196,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD (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); - } } // Called before sending input but after we've determined that this @@ -181,6 +223,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD if ((currentControllers & (1 << i)) == 0) { // Found an unused controller value currentControllers |= (1 << i); + + // Take this value out of the initial gamepad set + initialControllers &= ~(1 << i); + context.controllerNumber = i; context.reservedControllerNumber = true; break; @@ -201,6 +247,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD if ((currentControllers & (1 << i)) == 0) { // Found an unused controller value currentControllers |= (1 << i); + + // Take this value out of the initial gamepad set + initialControllers &= ~(1 << i); + context.controllerNumber = i; context.reservedControllerNumber = true; break; @@ -473,7 +523,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD private short getActiveControllerMask() { if (multiControllerEnabled) { - return currentControllers; + return (short)(currentControllers | initialControllers); } else { // Only Player 1 is active with multi-controller disabled 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 9616e7fb..1adaf582 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 @@ -157,7 +157,7 @@ public class UsbDriverService extends Service implements UsbDriverListener { } } - private boolean isRecognizedInputDevice(UsbDevice device) { + private static boolean isRecognizedInputDevice(UsbDevice device) { // On KitKat and later, we can determine if this VID and PID combo // matches an existing input device and defer to the built-in controller // support in that case. Prior to KitKat, we'll always return true to be safe. @@ -182,7 +182,7 @@ public class UsbDriverService extends Service implements UsbDriverListener { } } - private boolean shouldClaimDevice(UsbDevice device) { + public static boolean shouldClaimDevice(UsbDevice device) { // We always bind to XB1 controllers but only bind to XB360 controllers // if we know the kernel isn't already driving this device. return XboxOneController.canClaimDevice(device) ||