Implement support for rumble for Shield controllers on Shield devices

This commit is contained in:
Cameron Gutman
2022-06-09 18:51:23 -05:00
parent f55e4e0e01
commit a8479ccb5f
3 changed files with 321 additions and 0 deletions

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<String, Integer> tokenToDeviceIdMap = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, String> 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();
}
}
}