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 b47d8f79..019ece32 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -25,6 +25,7 @@ import com.limelight.LimeLog; import com.limelight.binding.input.driver.AbstractController; import com.limelight.binding.input.driver.UsbDriverListener; import com.limelight.binding.input.driver.UsbDriverService; +import com.limelight.binding.input.shield.ShieldControllerExtensionsHandler; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.input.ControllerPacket; import com.limelight.nvstream.input.MouseButtonPacket; @@ -61,6 +62,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD private final InputDeviceContext defaultContext = new InputDeviceContext(); private final GameGestures gestures; private final Vibrator deviceVibrator; + private final ShieldControllerExtensionsHandler shieldControllerExtensionsHandler; private boolean hasGameController; private final PreferenceConfiguration prefConfig; @@ -72,6 +74,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD this.gestures = gestures; this.prefConfig = prefConfig; this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE); + this.shieldControllerExtensionsHandler = new ShieldControllerExtensionsHandler(activityContext); int deadzonePercentage = prefConfig.deadzonePercentage; @@ -200,6 +203,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD deviceContext.destroy(); } + shieldControllerExtensionsHandler.destroy(); deviceVibrator.cancel(); } @@ -505,6 +509,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD } LimeLog.info(dev.toString()); + context.inputDevice = dev; context.name = devName; context.id = dev.getId(); context.external = isExternal(dev); @@ -1440,10 +1445,34 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD if (deviceContext.controllerNumber == controllerNumber) { foundMatchingDevice = true; + // Cancel pending rumble repeat timer if one exists + if (deviceContext.rumbleRepeatTimer != null) { + deviceContext.rumbleRepeatTimer.cancel(); + deviceContext.rumbleRepeatTimer = null; + } + + // 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); } + // On Shield devices, we can use their special API to rumble Shield controllers + else if (shieldControllerExtensionsHandler.rumble(deviceContext.inputDevice, lowFreqMotor, highFreqMotor)) { + vibrated = true; + + // The Shield controller can only rumble up to 1 second at a time, so we will call rumble again + // every 500 ms until the host PC gives us another rumble value. + if (lowFreqMotor != 0 || highFreqMotor != 0) { + deviceContext.rumbleRepeatTimer = new Timer("Rumble Repeat - "+deviceContext.name, true); + deviceContext.rumbleRepeatTimer.schedule(new TimerTask() { + @Override + public void run() { + shieldControllerExtensionsHandler.rumble(deviceContext.inputDevice, lowFreqMotor, highFreqMotor); + } + }, 500, 500); + } + } + // If all else fails, we have to try the old Vibrator API else if (deviceContext.vibrator != null) { vibrated = true; rumbleSingleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor); @@ -1931,6 +1960,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public String name; public VibratorManager vibratorManager; public Vibrator vibrator; + public InputDevice inputDevice; + public Timer rumbleRepeatTimer; public int leftStickXAxis = -1; public int leftStickYAxis = -1; @@ -1982,6 +2013,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD else if (vibrator != null) { vibrator.cancel(); } + + if (rumbleRepeatTimer != null) { + rumbleRepeatTimer.cancel(); + } } } diff --git a/app/src/main/java/com/limelight/binding/input/shield/IExposedControllerManagerListener.java b/app/src/main/java/com/limelight/binding/input/shield/IExposedControllerManagerListener.java new file mode 100644 index 00000000..68a34f72 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/shield/IExposedControllerManagerListener.java @@ -0,0 +1,51 @@ +package com.limelight.binding.input.shield; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; + +public interface IExposedControllerManagerListener extends IInterface { + void onDeviceAdded(String controllerToken); + void onDeviceChanged(String controllerToken, int i); + void onDeviceRemoved(String controllerToken); + + public static abstract class Stub extends Binder implements IExposedControllerManagerListener { + public Stub() { + attachInterface(this, "com.nvidia.blakepairing.IExposedControllerManagerListener"); + } + + @Override + public IBinder asBinder() { + return this; + } + + public boolean onTransact(int code, Parcel input, Parcel output, int flags) throws RemoteException { + switch (code) { + case 1: + input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener"); + onDeviceAdded(input.readString()); + break; + case 2: + input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener"); + onDeviceChanged(input.readString(), input.readInt()); + break; + case 3: + input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener"); + onDeviceRemoved(input.readString()); + break; + case 4: + case 5: + input.enforceInterface("com.nvidia.blakepairing.IExposedControllerManagerListener"); + // Don't care + break; + + default: + return super.onTransact(code, input, output, flags); + } + + return true; + } + } +} diff --git a/app/src/main/java/com/limelight/binding/input/shield/ShieldControllerExtensionsHandler.java b/app/src/main/java/com/limelight/binding/input/shield/ShieldControllerExtensionsHandler.java new file mode 100644 index 00000000..d14b96f1 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/input/shield/ShieldControllerExtensionsHandler.java @@ -0,0 +1,235 @@ +package com.limelight.binding.input.shield; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.view.InputDevice; + +import com.limelight.LimeLog; + +import java.util.concurrent.ConcurrentHashMap; + +public class ShieldControllerExtensionsHandler { + private Context context; + + private IBinder binder; + private ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + binder = iBinder; + + try { + listenerId = registerListener(); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + listenerId = 0; + tokenToDeviceIdMap.clear(); + deviceIdToTokenMap.clear(); + + binder = null; + } + }; + + // ConcurrentHashMap handles synchronization between the Binder thread adding/removing + // entries and callers on arbitrary threads that are doing device lookups. + // + // Since these are separate maps, they can be temporarily inconsistent (only one-way + // of the two-way mapping present). This is fine for our purposes here. + private ConcurrentHashMap tokenToDeviceIdMap = new ConcurrentHashMap<>(); + private ConcurrentHashMap deviceIdToTokenMap = new ConcurrentHashMap<>(); + + private int listenerId; + private IExposedControllerManagerListener.Stub controllerListener = new IExposedControllerManagerListener.Stub() { + @Override + public void onDeviceAdded(String controllerToken) { + try { + int inputDeviceId = getInputDeviceId(controllerToken); + + LimeLog.info("Shield controller added: " + controllerToken + " -> " + inputDeviceId); + + tokenToDeviceIdMap.put(controllerToken, inputDeviceId); + deviceIdToTokenMap.put(inputDeviceId, controllerToken); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + @Override + public void onDeviceChanged(String controllerToken, int i) { + LimeLog.info("Shield controller changed: " + controllerToken + " " + i); + } + + @Override + public void onDeviceRemoved(String controllerToken) { + LimeLog.info("Shield controller removed: " + controllerToken); + + Integer deviceId = tokenToDeviceIdMap.remove(controllerToken); + if (deviceId != null) { + deviceIdToTokenMap.remove(deviceId); + } + } + }; + + public ShieldControllerExtensionsHandler(Context context) { + this.context = context; + + Intent intent = new Intent(); + intent.setClassName("com.nvidia.blakepairing", "com.nvidia.blakepairing.AccessoryService"); + if (!context.bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE)) { + LimeLog.info("com.nvidia.blakepairing.AccessoryService is not available on this device"); + } + } + + public boolean rumble(InputDevice device, int lowFreqMotor, int highFreqMotor) { + String controllerToken = deviceIdToTokenMap.get(device.getId()); + if (controllerToken != null) { + try { + return rumble(controllerToken, lowFreqMotor, highFreqMotor); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + return false; + } + + public void destroy() { + tokenToDeviceIdMap.clear(); + deviceIdToTokenMap.clear(); + + if (listenerId != 0) { + try { + unregisterListener(listenerId); + } catch (RemoteException e) { + e.printStackTrace(); + } + listenerId = 0; + } + + if (binder != null) { + context.unbindService(serviceConnection); + binder = null; + } + } + + private int registerListener() throws RemoteException { + if (binder == null) { + return 0; + } + + Parcel input = Parcel.obtain(); + Parcel output = Parcel.obtain(); + try { + input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder"); + input.writeStrongBinder(controllerListener); + + binder.transact(20, input, output, 0); + + output.readException(); + return output.readInt(); + } finally { + input.recycle(); + output.recycle(); + } + } + + private boolean unregisterListener(int listenerId) throws RemoteException { + if (binder == null) { + return false; + } + + Parcel input = Parcel.obtain(); + Parcel output = Parcel.obtain(); + try { + input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder"); + input.writeInt(listenerId); + + binder.transact(21, input, output, 0); + + output.readException(); + return output.readInt() != 0; + } finally { + input.recycle(); + output.recycle(); + } + } + + private int getInputDeviceId(String controllerToken) throws RemoteException { + if (binder == null) { + return 0; + } + + Parcel input = Parcel.obtain(); + Parcel output = Parcel.obtain(); + try { + input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder"); + input.writeString(controllerToken); + + binder.transact(13, input, output, 0); + + output.readException(); + return output.readInt(); + } finally { + input.recycle(); + output.recycle(); + } + } + + // Rumble duration maximum of 1 second + private boolean rumble(String controllerToken, int lowFreqMotor, int highFreqMotor) throws RemoteException { + if (binder == null) { + return false; + } + + Parcel input = Parcel.obtain(); + Parcel output = Parcel.obtain(); + try { + input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder"); + input.writeString(controllerToken); + input.writeInt(lowFreqMotor); + input.writeInt(highFreqMotor); + + binder.transact(18, input, output, 0); + + output.readException(); + return output.readInt() != 0; + } finally { + input.recycle(); + output.recycle(); + } + } + + // Rumble duration maximum of 1.5 seconds + private boolean rumbleWithDuration(String controllerToken, int lowFreqMotor, int highFreqMotor, long durationMs) throws RemoteException { + if (binder == null) { + return false; + } + + Parcel input = Parcel.obtain(); + Parcel output = Parcel.obtain(); + try { + input.writeInterfaceToken("com.nvidia.blakepairing.IExposedControllerBinder"); + input.writeString(controllerToken); + input.writeInt(lowFreqMotor); + input.writeInt(highFreqMotor); + input.writeLong(durationMs); + + binder.transact(19, input, output, 0); + + output.readException(); + return output.readInt() != 0; + } finally { + input.recycle(); + output.recycle(); + } + } +}