Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
<!-- We don't need a MulticastLock on API level 34+ because we use NsdManager for mDNS -->
<uses-permission
Expand Down Expand Up @@ -37,6 +39,9 @@
<uses-feature
android:name="android.hardware.sensor.gyroscope"
android:required="false" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />

<!-- Disable legacy input emulation on ChromeOS -->
<uses-feature
Expand Down Expand Up @@ -224,6 +229,9 @@
<service
android:name=".binding.input.driver.UsbDriverService"
android:label="Usb Driver Service" />
<service
android:name=".binding.input.driver.BluetoothDriverService"
android:label="Bluetooth Driver Service" />

<activity
android:name=".HelpActivity"
Expand Down
58 changes: 57 additions & 1 deletion app/src/main/java/com/limelight/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@
import static com.limelight.utils.ServerHelper.getActiveDisplay;
import static com.limelight.utils.ServerHelper.getSecondaryDisplay;

import android.Manifest;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.core.content.ContextCompat;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.audio.AndroidAudioRenderer;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.GameInputDevice;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.capture.InputCaptureManager;
import com.limelight.binding.input.capture.InputCaptureProvider;
import com.limelight.binding.input.driver.BluetoothDriverService;
import com.limelight.binding.input.touch.AbsoluteTouchContext;
import com.limelight.binding.input.touch.RelativeTouchContext;
import com.limelight.binding.input.driver.UsbDriverService;
Expand Down Expand Up @@ -237,7 +242,7 @@ public class Game extends AppCompatActivity implements SurfaceHolder.Callback,
private WifiManager.WifiLock lowLatencyWifiLock;

private boolean connectedToUsbDriverService = false;
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
private final ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
UsbDriverService.UsbDriverBinder binder = (UsbDriverService.UsbDriverBinder) iBinder;
Expand All @@ -253,6 +258,32 @@ public void onServiceDisconnected(ComponentName componentName) {
}
};

private boolean connectedToBluetoothDriverService = false;
private final ServiceConnection bluetoothDriverServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
BluetoothDriverService.BluetoothDriverBinder binder = (BluetoothDriverService.BluetoothDriverBinder) iBinder;
binder.setListener(controllerHandler);
binder.setPreferenceConfiguration(prefConfig);
binder.start();
connectedToBluetoothDriverService = true;
}

@Override
public void onServiceDisconnected(ComponentName componentName) {
connectedToBluetoothDriverService = false;
}
};
private final ActivityResultLauncher<String> requestBluetoothPermissionLauncher =
this.registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
bindService(new Intent(this, BluetoothDriverService.class),
bluetoothDriverServiceConnection, Service.BIND_AUTO_CREATE);
} else {
Toast.makeText(this, getString(R.string.bluetooth_permission_denied), Toast.LENGTH_LONG).show();
}
});

public static final String EXTRA_HOST = "Host";
public static final String EXTRA_PORT = "Port";
public static final String EXTRA_HTTPS_PORT = "HttpsPort";
Expand Down Expand Up @@ -1736,6 +1767,10 @@ protected void onDestroy() {
// Unbind from the discovery service
unbindService(usbDriverServiceConnection);
}
if (connectedToBluetoothDriverService) {
// Unbind from the discovery service
unbindService(bluetoothDriverServiceConnection);
}

// Destroy the capture provider
inputCaptureProvider.destroy();
Expand Down Expand Up @@ -3709,6 +3744,14 @@ public void run() {
bindService(new Intent(this, UsbDriverService.class),
usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
}
if (prefConfig.bluetoothDriver) {
if (hasBluetoothPermission()) {
bindService(new Intent(this, BluetoothDriverService.class),
bluetoothDriverServiceConnection, Service.BIND_AUTO_CREATE);
} else {
requestBluetoothPermissionLauncher.launch(getBluetoothPermission());
}
}

// Report this shortcut being used (off the main thread to prevent ANRs)
ComputerDetails computer = new ComputerDetails();
Expand All @@ -3722,6 +3765,19 @@ public void run() {
}
}

private boolean hasBluetoothPermission() {
return ContextCompat.checkSelfPermission(getApplicationContext(), getBluetoothPermission())
== PackageManager.PERMISSION_GRANTED;
}

private String getBluetoothPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return Manifest.permission.BLUETOOTH_CONNECT;
} else {
return Manifest.permission.BLUETOOTH;
}
}

@Override
public void displayMessage(final String message) {
runOnUiThread(new Runnable() {
Expand Down
197 changes: 122 additions & 75 deletions app/src/main/java/com/limelight/binding/input/ControllerHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.input.driver.AbstractController;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.ControllerDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
Expand All @@ -56,7 +56,7 @@
import java.util.List;
import java.util.Map;

public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
public class ControllerHandler implements InputManager.InputDeviceListener, ControllerDriverListener {

private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;

Expand Down Expand Up @@ -2353,14 +2353,11 @@ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
}

public void handleSetMotionEventState(final short controllerNumber, final byte motionType, short reportRateHz) {
public void handleSetMotionEventState(final short controllerNumber, final byte motionType, final short reportRateHz) {
if (stopped) {
return;
}

// Report rate is restricted to <= 200 Hz without the HIGH_SAMPLING_RATE_SENSORS permission
reportRateHz = (short) Math.min(200, reportRateHz);

for (int i = 0; i < inputDeviceContexts.size() + usbDeviceContexts.size(); i++) {
InputDeviceContext deviceContext;
if (i < inputDeviceContexts.size()) {
Expand All @@ -2370,54 +2367,7 @@ public void handleSetMotionEventState(final short controllerNumber, final byte m
}

if (deviceContext.controllerNumber == controllerNumber) {
// Store the desired report rate even if we don't have sensors. In some cases,
// input devices can be reconfigured at runtime which results in a change where
// sensors disappear and reappear. By storing the desired report rate, we can
// reapply the desired motion sensor configuration after they reappear.
switch (motionType) {
case MoonBridge.LI_MOTION_TYPE_ACCEL:
deviceContext.accelReportRateHz = reportRateHz;
break;
case MoonBridge.LI_MOTION_TYPE_GYRO:
deviceContext.gyroReportRateHz = reportRateHz;
break;
}

backgroundThreadHandler.removeCallbacks(deviceContext.enableSensorRunnable);

SensorManager sm = deviceContext.sensorManager;
if (sm == null) {
continue;
}

switch (motionType) {
case MoonBridge.LI_MOTION_TYPE_ACCEL:
if (deviceContext.accelListener != null) {
sm.unregisterListener(deviceContext.accelListener);
deviceContext.accelListener = null;
}

// Enable the accelerometer if requested
Sensor accelSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (reportRateHz != 0 && accelSensor != null) {
deviceContext.accelListener = createSensorListener(controllerNumber, motionType, sm == deviceSensorManager);
sm.registerListener(deviceContext.accelListener, accelSensor, 1000000 / reportRateHz);
}
break;
case MoonBridge.LI_MOTION_TYPE_GYRO:
if (deviceContext.gyroListener != null) {
sm.unregisterListener(deviceContext.gyroListener);
deviceContext.gyroListener = null;
}

// Enable the gyroscope if requested
Sensor gyroSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
if (reportRateHz != 0 && gyroSensor != null) {
deviceContext.gyroListener = createSensorListener(controllerNumber, motionType, sm == deviceSensorManager);
sm.registerListener(deviceContext.gyroListener, gyroSensor, 1000000 / reportRateHz);
}
break;
}
deviceContext.setMotionEventState(motionType, reportRateHz);
break;
}
}
Expand All @@ -2429,30 +2379,17 @@ public void handleSetControllerLED(short controllerNumber, byte r, byte g, byte
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
for (int i = 0; i < inputDeviceContexts.size() + usbDeviceContexts.size(); i++) {
InputDeviceContext deviceContext;
if (i < inputDeviceContexts.size()) {
deviceContext = inputDeviceContexts.valueAt(i);
} else {
deviceContext = usbDeviceContexts.valueAt(i - inputDeviceContexts.size());
}

// Ignore input devices without an RGB LED
if (deviceContext.controllerNumber == controllerNumber && deviceContext.hasRgbLed) {
// Create a new light session if one doesn't already exist
if (deviceContext.lightsSession == null) {
deviceContext.lightsSession = deviceContext.inputDevice.getLightsManager().openSession();
}

// Convert the RGB components into the integer value that LightState uses
int argbValue = 0xFF000000 | ((r << 16) & 0xFF0000) | ((g << 8) & 0xFF00) | (b & 0xFF);
LightState lightState = new LightState.Builder().setColor(argbValue).build();

// Set the RGB value for each RGB-controllable LED on the device
LightsRequest.Builder lightsRequestBuilder = new LightsRequest.Builder();
for (Light light : deviceContext.inputDevice.getLightsManager().getLights()) {
if (light.hasRgbControl()) {
lightsRequestBuilder.addLight(light, lightState);
}
}

// Apply the LED changes
deviceContext.lightsSession.requestLights(lightsRequestBuilder.build());
deviceContext.setControllerLED(r, g, b);
}
}
}
Expand Down Expand Up @@ -3007,6 +2944,16 @@ public void reportControllerMotion(int controllerId, byte motionType, float moti
conn.sendControllerMotionEvent((byte)context.controllerNumber, motionType, motionX, motionY, motionZ);
}

@Override
public void reportBatteryState(int controllerId, byte batteryState, byte batteryPercentage) {
GenericControllerContext context = usbDeviceContexts.get(controllerId);
if (context == null) {
return;
}

conn.sendControllerBatteryEvent((byte)context.controllerNumber, batteryState, batteryPercentage);
}

@Override
public void deviceRemoved(AbstractController controller) {
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
Expand Down Expand Up @@ -3404,6 +3351,86 @@ public void migrateContext(InputDeviceContext oldContext) {
backgroundThreadHandler.post(batteryStateUpdateRunnable);
}

public void setMotionEventState(final byte motionType, short reportRateHz) {
// Report rate is restricted to <= 200 Hz without the HIGH_SAMPLING_RATE_SENSORS permission
reportRateHz = (short) Math.min(200, reportRateHz);

// Store the desired report rate even if we don't have sensors. In some cases,
// input devices can be reconfigured at runtime which results in a change where
// sensors disappear and reappear. By storing the desired report rate, we can
// reapply the desired motion sensor configuration after they reappear.
switch (motionType) {
case MoonBridge.LI_MOTION_TYPE_ACCEL:
accelReportRateHz = reportRateHz;
break;
case MoonBridge.LI_MOTION_TYPE_GYRO:
gyroReportRateHz = reportRateHz;
break;
}

backgroundThreadHandler.removeCallbacks(enableSensorRunnable);

SensorManager sm = sensorManager;
if (sm == null) {
return;
}

switch (motionType) {
case MoonBridge.LI_MOTION_TYPE_ACCEL:
if (accelListener != null) {
sm.unregisterListener(accelListener);
accelListener = null;
}

// Enable the accelerometer if requested
Sensor accelSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (reportRateHz != 0 && accelSensor != null) {
accelListener = createSensorListener(controllerNumber, motionType, sm == deviceSensorManager);
sm.registerListener(accelListener, accelSensor, 1000000 / reportRateHz);
}
break;
case MoonBridge.LI_MOTION_TYPE_GYRO:
if (gyroListener != null) {
sm.unregisterListener(gyroListener);
gyroListener = null;
}

// Enable the gyroscope if requested
Sensor gyroSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
if (reportRateHz != 0 && gyroSensor != null) {
gyroListener = createSensorListener(controllerNumber, motionType, sm == deviceSensorManager);
sm.registerListener(gyroListener, gyroSensor, 1000000 / reportRateHz);
}
break;
}
}

public void setControllerLED(byte r, byte g, byte b) {
if (!hasRgbLed || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return;
}

// Create a new light session if one doesn't already exist
if (lightsSession == null) {
lightsSession = inputDevice.getLightsManager().openSession();
}

// Convert the RGB components into the integer value that LightState uses
int argbValue = 0xFF000000 | ((r << 16) & 0xFF0000) | ((g << 8) & 0xFF00) | (b & 0xFF);
LightState lightState = new LightState.Builder().setColor(argbValue).build();

// Set the RGB value for each RGB-controllable LED on the device
LightsRequest.Builder lightsRequestBuilder = new LightsRequest.Builder();
for (Light light : inputDevice.getLightsManager().getLights()) {
if (light.hasRgbControl()) {
lightsRequestBuilder.addLight(light, lightState);
}
}

// Apply the LED changes
lightsSession.requestLights(lightsRequestBuilder.build());
}

public void disableSensors() {
// Stop any pending enablement
backgroundThreadHandler.removeCallbacks(enableSensorRunnable);
Expand Down Expand Up @@ -3465,8 +3492,28 @@ public void sendControllerArrival() {
});
}

hasRgbLed = (capabilities & MoonBridge.LI_CCAP_RGB_LED) != 0;

conn.sendControllerArrivalEvent((byte)controllerNumber, getActiveControllerMask(),
type, device.getSupportedButtonFlags(), capabilities);
}

@Override
public void setMotionEventState(byte motionType, short reportRateHz) {
if (((device.getCapabilities() & MoonBridge.LI_CCAP_GYRO) | (device.getCapabilities() & MoonBridge.LI_CCAP_ACCEL)) == 0) {
super.setMotionEventState(motionType, reportRateHz);
} else {
device.setMotionEventState(motionType, reportRateHz);
}
}

@Override
public void setControllerLED(byte r, byte g, byte b) {
if ((device.getCapabilities() & MoonBridge.LI_CCAP_RGB_LED) == 0) {
super.setControllerLED(r, g, b);
} else {
device.setControllerLED(r, g, b);
}
}
}
}
Loading