diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b301979c..a43a2e23 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,12 @@ + + = Build.VERSION_CODES.S) { @@ -416,6 +418,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD LimeLog.info("Not reserving a controller number"); context.controllerNumber = 0; } + + // If the gamepad doesn't have motion sensors, use the on-device sensors as a fallback for player 1 + if (prefConfig.gamepadMotionSensorsFallbackToDevice && context.controllerNumber == 0 && devContext.sensorManager == null) { + devContext.sensorManager = deviceSensorManager; + } } else { if (prefConfig.multiController) { @@ -648,6 +655,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD } } + // On Android 12, we can try to use the InputDevice's sensors. This may not work if the + // Linux kernel version doesn't have motion sensor support, which is common for third-party + // gamepads. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && prefConfig.gamepadMotionSensors) { + if (dev.getSensorManager().getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null || dev.getSensorManager().getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) { + context.sensorManager = dev.getSensorManager(); + } + } + // Detect if the gamepad has Mode and Select buttons according to the Android key layouts. // We do this first because other codepaths below may override these defaults. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { @@ -1955,11 +1971,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD } public void handleSetMotionEventState(final short controllerNumber, final byte motionType, short reportRateHz) { - // Don't use motion sensors if the user turned them off - if (!prefConfig.gamepadMotionSensors) { - return; - } - // Report rate is restricted to <= 200 Hz without the HIGH_SAMPLING_RATE_SENSORS permission reportRateHz = (short) Math.min(200, reportRateHz); @@ -2004,51 +2015,52 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public void onAccuracyChanged(Sensor sensor, int accuracy) {} }; - 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(); i++) { + InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i); - if (deviceContext.controllerNumber == controllerNumber) { - SensorManager sm = deviceContext.inputDevice.getSensorManager(); - - 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, false); - if (reportRateHz != 0 && accelSensor != null) { - sm.registerListener(newSensorListener, accelSensor, 1000000 / reportRateHz); - deviceContext.accelListener = newSensorListener; - deviceContext.accelReportRateHz = reportRateHz; - } - else { - deviceContext.accelReportRateHz = 0; - } - 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, false); - if (reportRateHz != 0 && gyroSensor != null) { - sm.registerListener(newSensorListener, gyroSensor,1000000 / reportRateHz); - deviceContext.gyroListener = newSensorListener; - deviceContext.gyroReportRateHz = reportRateHz; - } - else { - deviceContext.gyroReportRateHz = 0; - } - break; - } - break; + if (deviceContext.controllerNumber == controllerNumber) { + 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, false); + if (reportRateHz != 0 && accelSensor != null) { + sm.registerListener(newSensorListener, accelSensor, 1000000 / reportRateHz); + deviceContext.accelListener = newSensorListener; + deviceContext.accelReportRateHz = reportRateHz; + } + else { + deviceContext.accelReportRateHz = 0; + } + 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, false); + if (reportRateHz != 0 && gyroSensor != null) { + sm.registerListener(newSensorListener, gyroSensor,1000000 / reportRateHz); + deviceContext.gyroListener = newSensorListener; + deviceContext.gyroReportRateHz = reportRateHz; + } + else { + deviceContext.gyroReportRateHz = 0; + } + break; + } + break; } } } @@ -2663,6 +2675,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public short lowFreqMotor, highFreqMotor; public short leftTriggerMotor, rightTriggerMotor; + public SensorManager sensorManager; public SensorEventListener gyroListener; public short gyroReportRateHz; public SensorEventListener accelListener; @@ -2743,14 +2756,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD vibrator.cancel(); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (gyroListener != null) { - inputDevice.getSensorManager().unregisterListener(gyroListener); - } - if (accelListener != null) { - inputDevice.getSensorManager().unregisterListener(accelListener); - } + if (gyroListener != null) { + sensorManager.unregisterListener(gyroListener); + } + if (accelListener != null) { + sensorManager.unregisterListener(accelListener); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (lightsSession != null) { lightsSession.close(); } @@ -2820,13 +2833,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD capabilities |= MoonBridge.LI_CCAP_RUMBLE; } - if (inputDevice.getSensorManager().getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null) { - capabilities |= MoonBridge.LI_CCAP_ACCEL; - } - if (inputDevice.getSensorManager().getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) { - capabilities |= MoonBridge.LI_CCAP_GYRO; - } - if (inputDevice.getBatteryState().isPresent()) { capabilities |= MoonBridge.LI_CCAP_BATTERY_STATE; } @@ -2848,6 +2854,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD capabilities |= MoonBridge.LI_CCAP_ANALOG_TRIGGERS; } + // Report sensors if the input device has them or we're using built-in sensors for a built-in controller + if (sensorManager != null && sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null) { + capabilities |= MoonBridge.LI_CCAP_ACCEL; + } + if (sensorManager != null && sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) { + capabilities |= MoonBridge.LI_CCAP_GYRO; + } + // We can perform basic rumble with any vibrator if (vibrator != null) { capabilities |= MoonBridge.LI_CCAP_RUMBLE; @@ -2878,15 +2892,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public void migrateContext(InputDeviceContext oldContext) { // Take ownership of the sensor and light sessions if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - this.gyroReportRateHz = oldContext.gyroReportRateHz; - this.accelReportRateHz = oldContext.accelReportRateHz; this.lightsSession = oldContext.lightsSession; - this.gyroListener = oldContext.gyroListener; - this.accelListener = oldContext.accelListener; oldContext.lightsSession = null; - oldContext.gyroListener = null; - oldContext.accelListener = null; } + this.gyroListener = oldContext.gyroListener; + this.accelListener = oldContext.accelListener; + this.gyroReportRateHz = oldContext.gyroReportRateHz; + this.accelReportRateHz = oldContext.accelReportRateHz; + oldContext.gyroListener = null; + oldContext.accelListener = null; // Don't release the controller number, because we will carry it over if it is present. // We also want to make sure the change is invisible to the host PC to avoid an add/remove @@ -2898,6 +2912,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD this.reservedControllerNumber = oldContext.reservedControllerNumber; this.controllerNumber = oldContext.controllerNumber; + // We may have set this device to use the built-in sensor manager. If so, do that again. + if (oldContext.sensorManager == deviceSensorManager) { + this.sensorManager = deviceSensorManager; + } + // Refresh battery state and start the battery state polling again sendControllerBatteryPacket(this); handler.postDelayed(batteryStateUpdateRunnable, BATTERY_RECHECK_INTERVAL_MS); @@ -2905,20 +2924,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public void disableSensors() { // Unregister all sensor listeners - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (gyroListener != null) { - inputDevice.getSensorManager().unregisterListener(gyroListener); - gyroListener = null; + if (gyroListener != null) { + sensorManager.unregisterListener(gyroListener); + gyroListener = null; - // Send a gyro event to ensure the virtual controller is stationary - conn.sendControllerMotionEvent((byte) controllerNumber, MoonBridge.LI_MOTION_TYPE_GYRO, 0.f, 0.f, 0.f); - } - if (accelListener != null) { - inputDevice.getSensorManager().unregisterListener(accelListener); - accelListener = null; + // Send a gyro event to ensure the virtual controller is stationary + conn.sendControllerMotionEvent((byte) controllerNumber, MoonBridge.LI_MOTION_TYPE_GYRO, 0.f, 0.f, 0.f); + } + if (accelListener != null) { + sensorManager.unregisterListener(accelListener); + accelListener = null; - // We leave the acceleration as-is to preserve the attitude of the controller - } + // We leave the acceleration as-is to preserve the attitude of the controller } } diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index a23eea68..8d64a892 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -58,6 +58,7 @@ public class PreferenceConfiguration { private static final String FULL_RANGE_PREF_STRING = "checkbox_full_range"; private static final String GAMEPAD_TOUCHPAD_AS_MOUSE_PREF_STRING = "checkbox_gamepad_touchpad_as_mouse"; private static final String GAMEPAD_MOTION_SENSORS_PREF_STRING = "checkbox_gamepad_motion_sensors"; + private static final String GAMEPAD_MOTION_FALLBACK_PREF_STRING = "checkbox_gamepad_motion_fallback"; static final String DEFAULT_RESOLUTION = "1280x720"; static final String DEFAULT_FPS = "60"; @@ -94,6 +95,7 @@ public class PreferenceConfiguration { private static final boolean DEFAULT_FULL_RANGE = false; private static final boolean DEFAULT_GAMEPAD_TOUCHPAD_AS_MOUSE = false; private static final boolean DEFAULT_GAMEPAD_MOTION_SENSORS = true; + private static final boolean DEFAULT_GAMEPAD_MOTION_FALLBACK = false; public static final int FRAME_PACING_MIN_LATENCY = 0; public static final int FRAME_PACING_BALANCED = 1; @@ -137,6 +139,7 @@ public class PreferenceConfiguration { public boolean fullRange; public boolean gamepadMotionSensors; public boolean gamepadTouchpadAsMouse; + public boolean gamepadMotionSensorsFallbackToDevice; public static boolean isNativeResolution(int width, int height) { // It's not a native resolution if it matches an existing resolution option @@ -525,6 +528,7 @@ public class PreferenceConfiguration { config.fullRange = prefs.getBoolean(FULL_RANGE_PREF_STRING, DEFAULT_FULL_RANGE); config.gamepadTouchpadAsMouse = prefs.getBoolean(GAMEPAD_TOUCHPAD_AS_MOUSE_PREF_STRING, DEFAULT_GAMEPAD_TOUCHPAD_AS_MOUSE); config.gamepadMotionSensors = prefs.getBoolean(GAMEPAD_MOTION_SENSORS_PREF_STRING, DEFAULT_GAMEPAD_MOTION_SENSORS); + config.gamepadMotionSensorsFallbackToDevice = prefs.getBoolean(GAMEPAD_MOTION_FALLBACK_PREF_STRING, DEFAULT_GAMEPAD_MOTION_FALLBACK); return config; } diff --git a/app/src/main/java/com/limelight/preferences/StreamSettings.java b/app/src/main/java/com/limelight/preferences/StreamSettings.java index 651a657a..588f3267 100644 --- a/app/src/main/java/com/limelight/preferences/StreamSettings.java +++ b/app/src/main/java/com/limelight/preferences/StreamSettings.java @@ -296,6 +296,14 @@ public class StreamSettings extends Activity { category.removePreference(findPreference("checkbox_gamepad_motion_sensors")); } + // Hide gamepad motion sensor fallback option if the device has no gyro or accelerometer + if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER) && + !getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE)) { + PreferenceCategory category = + (PreferenceCategory) findPreference("category_gamepad_settings"); + category.removePreference(findPreference("checkbox_gamepad_motion_fallback")); + } + // Remove PiP mode on devices pre-Oreo, where the feature is not available (some low RAM devices), // and on Fire OS where it violates the Amazon App Store guidelines for some reason. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 307c0076..36acd907 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -187,6 +187,8 @@ Forces gamepad touchpad input to control the host mouse, even when emulating a gamepad with a touchpad. Allow use of gamepad motion sensors Enables supported hosts to request motion sensor data when emulating a gamepad with motion sensors. Disabling may slightly reduce power and network usage if motion sensors are not being used in game. + Emulate gamepad motion support using device sensors + Uses your device\'s built-in motion sensors if gamepad sensors are not supported by your connected gamepad or your Android version.\nNote: Enabling this option may cause your controller to appear on the host as a PlayStation controller. Input Settings Use the touchscreen as a trackpad diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index d169b555..5edc62fa 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -105,7 +105,11 @@ android:title="@string/title_checkbox_gamepad_motion_sensors" android:summary="@string/summary_checkbox_gamepad_motion_sensors" android:defaultValue="true" /> - +