mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-04-03 06:26:16 +00:00
Use new ShieldControllerExtensions library for Shield Controller rumble support
https://github.com/cgutman/ShieldControllerExtensions
This commit is contained in:
@@ -130,4 +130,5 @@ dependencies {
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.13'
|
||||
implementation 'com.squareup.okio:okio:1.17.5'
|
||||
implementation 'org.jmdns:jmdns:3.5.7'
|
||||
implementation 'com.github.cgutman:ShieldControllerExtensions:1.0'
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ 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;
|
||||
@@ -33,6 +32,8 @@ import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.GameGestures;
|
||||
import com.limelight.utils.Vector2d;
|
||||
|
||||
import org.cgutman.shieldcontrollerextensions.SceManager;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
@@ -62,7 +63,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 final SceManager sceManager;
|
||||
private boolean hasGameController;
|
||||
|
||||
private final PreferenceConfiguration prefConfig;
|
||||
@@ -74,7 +75,9 @@ 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);
|
||||
|
||||
this.sceManager = new SceManager(activityContext);
|
||||
this.sceManager.start();
|
||||
|
||||
int deadzonePercentage = prefConfig.deadzonePercentage;
|
||||
|
||||
@@ -203,7 +206,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
deviceContext.destroy();
|
||||
}
|
||||
|
||||
shieldControllerExtensionsHandler.destroy();
|
||||
sceManager.stop();
|
||||
deviceVibrator.cancel();
|
||||
}
|
||||
|
||||
@@ -1445,32 +1448,14 @@ 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)) {
|
||||
else if (sceManager.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) {
|
||||
@@ -1961,7 +1946,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
public VibratorManager vibratorManager;
|
||||
public Vibrator vibrator;
|
||||
public InputDevice inputDevice;
|
||||
public Timer rumbleRepeatTimer;
|
||||
|
||||
public int leftStickXAxis = -1;
|
||||
public int leftStickYAxis = -1;
|
||||
@@ -2013,10 +1997,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
else if (vibrator != null) {
|
||||
vibrator.cancel();
|
||||
}
|
||||
|
||||
if (rumbleRepeatTimer != null) {
|
||||
rumbleRepeatTimer.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
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.hardware.input.InputManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.RemoteException;
|
||||
import android.view.InputDevice;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShieldControllerExtensionsHandler implements InputManager.InputDeviceListener {
|
||||
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 AtomicBoolean needsRefresh = new AtomicBoolean(false);
|
||||
|
||||
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;
|
||||
|
||||
InputManager inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.registerInputDeviceListener(this, null);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setClassName("com.nvidia.blakepairing", "com.nvidia.blakepairing.AccessoryService");
|
||||
try {
|
||||
// The docs say to call unbindService() even if the bindService() call returns false
|
||||
// or throws a SecurityException.
|
||||
if (!context.bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE)) {
|
||||
LimeLog.info("com.nvidia.blakepairing.AccessoryService is not available on this device");
|
||||
context.unbindService(serviceConnection);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
context.unbindService(serviceConnection);
|
||||
}
|
||||
}
|
||||
|
||||
private String getControllerToken(InputDevice device) {
|
||||
// Refresh device ID <-> token mappings if one of our devices was removed
|
||||
if (needsRefresh.compareAndSet(true, false)) {
|
||||
try {
|
||||
LimeLog.info("Refreshing controller token mappings");
|
||||
|
||||
// We have to enumerate tokenToDeviceIdMap rather than deviceIdToTokenMap
|
||||
// because we remove the deviceIdToTokenMap entry when the device goes away.
|
||||
HashMap<String, Integer> newTokenToDeviceIdMap = new HashMap<>();
|
||||
HashMap<Integer, String> newDeviceIdToTokenMap = new HashMap<>();
|
||||
for (String existingToken : tokenToDeviceIdMap.keySet()) {
|
||||
int deviceId = getInputDeviceId(existingToken);
|
||||
if (deviceId != 0) {
|
||||
newTokenToDeviceIdMap.put(existingToken, deviceId);
|
||||
newDeviceIdToTokenMap.put(deviceId, existingToken);
|
||||
}
|
||||
}
|
||||
|
||||
tokenToDeviceIdMap.clear();
|
||||
deviceIdToTokenMap.clear();
|
||||
|
||||
tokenToDeviceIdMap.putAll(newTokenToDeviceIdMap);
|
||||
deviceIdToTokenMap.putAll(newDeviceIdToTokenMap);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return deviceIdToTokenMap.get(device.getId());
|
||||
}
|
||||
|
||||
public boolean rumble(InputDevice device, int lowFreqMotor, int highFreqMotor) {
|
||||
String controllerToken = getControllerToken(device);
|
||||
if (controllerToken != null) {
|
||||
try {
|
||||
return rumble(controllerToken, lowFreqMotor, highFreqMotor);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
InputManager inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.unregisterInputDeviceListener(this);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceAdded(int deviceId) {}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int deviceId) {
|
||||
// Remove the device ID to token mapping, but leave the token mapping to device ID
|
||||
// mapping so we will re-enumerate it when we next try to rumble a controller.
|
||||
if (deviceIdToTokenMap.remove(deviceId) != null) {
|
||||
needsRefresh.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,6 @@ allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user