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 6e82c66a..c0329792 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -522,8 +522,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD context.productId = dev.getProductId(); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasDualAmplitudeControlledRumbleVibrators(dev.getVibratorManager())) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasQuadAmplitudeControlledRumbleVibrators(dev.getVibratorManager())) { context.vibratorManager = dev.getVibratorManager(); + context.quadVibrators = true; + } + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasDualAmplitudeControlledRumbleVibrators(dev.getVibratorManager())) { + context.vibratorManager = dev.getVibratorManager(); + context.quadVibrators = false; } else if (dev.getVibrator().hasVibrator()) { context.vibrator = dev.getVibrator(); @@ -1363,6 +1368,64 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD vm.vibrate(combo.combine(), vibrationAttributes.build()); } + @TargetApi(31) + private boolean hasQuadAmplitudeControlledRumbleVibrators(VibratorManager vm) { + int[] vibratorIds = vm.getVibratorIds(); + + // There must be exactly 4 vibrators on this device + if (vibratorIds.length != 4) { + return false; + } + + // All vibrators must have amplitude control + for (int vid : vibratorIds) { + if (!vm.getVibrator(vid).hasAmplitudeControl()) { + return false; + } + } + + return true; + } + + // This must only be called if hasQuadAmplitudeControlledRumbleVibrators() is true! + @TargetApi(31) + private void rumbleQuadVibrators(VibratorManager vm, short lowFreqMotor, short highFreqMotor, short leftTrigger, short rightTrigger) { + // Normalize motor values to 0-255 amplitudes for VibrationManager + highFreqMotor = (short)((highFreqMotor >> 8) & 0xFF); + lowFreqMotor = (short)((lowFreqMotor >> 8) & 0xFF); + leftTrigger = (short)((leftTrigger >> 8) & 0xFF); + rightTrigger = (short)((rightTrigger >> 8) & 0xFF); + + // If they're all zero, we can just call cancel(). + if (lowFreqMotor == 0 && highFreqMotor == 0 && leftTrigger == 0 && rightTrigger == 0) { + vm.cancel(); + return; + } + + // This is a guess based upon the behavior of FF_RUMBLE, but untested due to lack of Linux + // support for trigger rumble! + int[] vibratorIds = vm.getVibratorIds(); + int[] vibratorAmplitudes = new int[] { highFreqMotor, lowFreqMotor, leftTrigger, rightTrigger }; + + CombinedVibration.ParallelCombination combo = CombinedVibration.startParallel(); + + for (int i = 0; i < vibratorIds.length; i++) { + // It's illegal to create a VibrationEffect with an amplitude of 0. + // Simply excluding that vibrator from our ParallelCombination will turn it off. + if (vibratorAmplitudes[i] != 0) { + combo.addVibrator(vibratorIds[i], VibrationEffect.createOneShot(60000, vibratorAmplitudes[i])); + } + } + + VibrationAttributes.Builder vibrationAttributes = new VibrationAttributes.Builder(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + vibrationAttributes.setUsage(VibrationAttributes.USAGE_MEDIA); + } + + vm.vibrate(combo.combine(), vibrationAttributes.build()); + } + private void rumbleSingleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) { // Since we can only use a single amplitude value, compute the desired amplitude // by taking 80% of the big motor and 33% of the small motor, then capping to 255. @@ -1433,19 +1496,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD if (deviceContext.controllerNumber == controllerNumber) { foundMatchingDevice = true; + deviceContext.lowFreqMotor = lowFreqMotor; + deviceContext.highFreqMotor = highFreqMotor; + // Prefer the documented Android 12 rumble API which can handle dual vibrators on PS/Xbox controllers if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && deviceContext.vibratorManager != null) { vibrated = true; - rumbleDualVibrators(deviceContext.vibratorManager, lowFreqMotor, highFreqMotor); + if (deviceContext.quadVibrators) { + rumbleQuadVibrators(deviceContext.vibratorManager, + deviceContext.lowFreqMotor, deviceContext.highFreqMotor, + deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor); + } + else { + rumbleDualVibrators(deviceContext.vibratorManager, + deviceContext.lowFreqMotor, deviceContext.highFreqMotor); + } } // On Shield devices, we can use their special API to rumble Shield controllers - else if (sceManager.rumble(deviceContext.inputDevice, lowFreqMotor, highFreqMotor)) { + else if (sceManager.rumble(deviceContext.inputDevice, deviceContext.lowFreqMotor, deviceContext.highFreqMotor)) { vibrated = true; } // If all else fails, we have to try the old Vibrator API else if (deviceContext.vibrator != null) { vibrated = true; - rumbleSingleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor); + rumbleSingleVibrator(deviceContext.vibrator, deviceContext.lowFreqMotor, deviceContext.highFreqMotor); } } } @@ -1455,7 +1529,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD if (deviceContext.controllerNumber == controllerNumber) { foundMatchingDevice = vibrated = true; - deviceContext.device.rumble((short)lowFreqMotor, (short)highFreqMotor); + deviceContext.device.rumble(lowFreqMotor, highFreqMotor); } } @@ -1476,7 +1550,28 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD } public void handleRumbleTriggers(short controllerNumber, short leftTrigger, short rightTrigger) { - // TODO + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + for (int i = 0; i < inputDeviceContexts.size(); i++) { + InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i); + + deviceContext.leftTriggerMotor = leftTrigger; + deviceContext.rightTriggerMotor = rightTrigger; + + if (deviceContext.controllerNumber == controllerNumber && deviceContext.quadVibrators) { + rumbleQuadVibrators(deviceContext.vibratorManager, + deviceContext.lowFreqMotor, deviceContext.highFreqMotor, + deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor); + } + } + } + + for (int i = 0; i < usbDeviceContexts.size(); i++) { + UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i); + + if (deviceContext.controllerNumber == controllerNumber) { + deviceContext.device.rumbleTriggers(leftTrigger, rightTrigger); + } + } } public void handleSetMotionEventState(short controllerNumber, byte motionType, short reportRateHz) { @@ -1947,6 +2042,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public String name; public VibratorManager vibratorManager; public Vibrator vibrator; + public boolean quadVibrators; + public short lowFreqMotor, highFreqMotor; + public short leftTriggerMotor, rightTriggerMotor; + public InputDevice inputDevice; public int leftStickXAxis = -1; diff --git a/app/src/main/java/com/limelight/binding/input/driver/AbstractController.java b/app/src/main/java/com/limelight/binding/input/driver/AbstractController.java index 24095723..fe31ca67 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/AbstractController.java +++ b/app/src/main/java/com/limelight/binding/input/driver/AbstractController.java @@ -8,10 +8,12 @@ public abstract class AbstractController { private UsbDriverListener listener; - protected short buttonFlags; + protected int buttonFlags, supportedButtonFlags; protected float leftTrigger, rightTrigger; protected float rightStickX, rightStickY; protected float leftStickX, leftStickY; + protected short capabilities; + protected byte type; public int getControllerId() { return deviceId; @@ -25,6 +27,18 @@ public abstract class AbstractController { return productId; } + public int getSupportedButtonFlags() { + return supportedButtonFlags; + } + + public short getCapabilities() { + return capabilities; + } + + public byte getType() { + return type; + } + protected void setButtonFlag(int buttonFlag, int data) { if (data != 0) { buttonFlags |= buttonFlag; @@ -51,6 +65,8 @@ public abstract class AbstractController { public abstract void rumble(short lowFreqMotor, short highFreqMotor); + public abstract void rumbleTriggers(short leftTrigger, short rightTrigger); + protected void notifyDeviceRemoved() { listener.deviceRemoved(this); } diff --git a/app/src/main/java/com/limelight/binding/input/driver/AbstractXboxController.java b/app/src/main/java/com/limelight/binding/input/driver/AbstractXboxController.java index 4755af1d..4525b4fb 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/AbstractXboxController.java +++ b/app/src/main/java/com/limelight/binding/input/driver/AbstractXboxController.java @@ -8,6 +8,8 @@ import android.hardware.usb.UsbInterface; import android.os.SystemClock; import com.limelight.LimeLog; +import com.limelight.nvstream.input.ControllerPacket; +import com.limelight.nvstream.jni.MoonBridge; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -25,6 +27,14 @@ public abstract class AbstractXboxController extends AbstractController { super(deviceId, listener, device.getVendorId(), device.getProductId()); this.device = device; this.connection = connection; + this.type = MoonBridge.LI_CTYPE_XBOX; + this.capabilities = MoonBridge.LI_CCAP_ANALOG_TRIGGERS | MoonBridge.LI_CCAP_RUMBLE; + this.buttonFlags = + ControllerPacket.A_FLAG | ControllerPacket.B_FLAG | ControllerPacket.X_FLAG | ControllerPacket.Y_FLAG | + ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG | ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG | + ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG | + ControllerPacket.LS_CLK_FLAG | ControllerPacket.RS_CLK_FLAG | + ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.SPECIAL_BUTTON_FLAG; } private Thread createInputThread() { diff --git a/app/src/main/java/com/limelight/binding/input/driver/Xbox360Controller.java b/app/src/main/java/com/limelight/binding/input/driver/Xbox360Controller.java index 05f102a4..c1539129 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/Xbox360Controller.java +++ b/app/src/main/java/com/limelight/binding/input/driver/Xbox360Controller.java @@ -156,4 +156,9 @@ public class Xbox360Controller extends AbstractXboxController { LimeLog.warning("Rumble transfer failed: "+res); } } + + @Override + public void rumbleTriggers(short leftTrigger, short rightTrigger) { + // Trigger motors not present on Xbox 360 controllers + } } diff --git a/app/src/main/java/com/limelight/binding/input/driver/Xbox360WirelessDongle.java b/app/src/main/java/com/limelight/binding/input/driver/Xbox360WirelessDongle.java index 52291dc8..632f1825 100644 --- a/app/src/main/java/com/limelight/binding/input/driver/Xbox360WirelessDongle.java +++ b/app/src/main/java/com/limelight/binding/input/driver/Xbox360WirelessDongle.java @@ -142,4 +142,9 @@ public class Xbox360WirelessDongle extends AbstractController { public void rumble(short lowFreqMotor, short highFreqMotor) { // Unreachable. } + + @Override + public void rumbleTriggers(short leftTrigger, short rightTrigger) { + // Unreachable. + } } 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 4bf9cd49..b84f2cc7 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 @@ -6,6 +6,7 @@ import android.hardware.usb.UsbDeviceConnection; import com.limelight.LimeLog; import com.limelight.nvstream.input.ControllerPacket; +import com.limelight.nvstream.jni.MoonBridge; import java.nio.ByteBuffer; import java.util.Arrays; @@ -54,9 +55,14 @@ public class XboxOneController extends AbstractXboxController { }; private byte seqNum = 0; + private short lowFreqMotor = 0; + private short highFreqMotor = 0; + private short leftTriggerMotor = 0; + private short rightTriggerMotor = 0; public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) { super(device, connection, deviceId, listener); + capabilities |= MoonBridge.LI_CCAP_TRIGGER_RUMBLE; } private void processButtons(ByteBuffer buffer) { @@ -176,12 +182,14 @@ public class XboxOneController extends AbstractXboxController { return true; } - @Override - public void rumble(short lowFreqMotor, short highFreqMotor) { + private void sendRumblePacket() { byte[] data = { 0x09, 0x00, seqNum++, 0x09, 0x00, - 0x0F, 0x00, 0x00, - (byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9), + 0x0F, + (byte)(leftTriggerMotor >> 9), + (byte)(rightTriggerMotor >> 9), + (byte)(lowFreqMotor >> 9), + (byte)(highFreqMotor >> 9), (byte)0xFF, 0x00, (byte)0xFF }; int res = connection.bulkTransfer(outEndpt, data, data.length, 100); @@ -190,6 +198,20 @@ public class XboxOneController extends AbstractXboxController { } } + @Override + public void rumble(short lowFreqMotor, short highFreqMotor) { + this.lowFreqMotor = lowFreqMotor; + this.highFreqMotor = highFreqMotor; + sendRumblePacket(); + } + + @Override + public void rumbleTriggers(short leftTrigger, short rightTrigger) { + this.leftTriggerMotor = leftTrigger; + this.rightTriggerMotor = rightTrigger; + sendRumblePacket(); + } + private static class InitPacket { final int vendorId; final int productId;