diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 4eed2bbc..75f501e4 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -2257,6 +2257,11 @@ public class Game extends Activity implements SurfaceHolder.Callback, controllerHandler.handleSetMotionEventState(controllerNumber, motionType, reportRateHz); } + @Override + public void setControllerLED(short controllerNumber, byte r, byte g, byte b) { + controllerHandler.handleSetControllerLED(controllerNumber, r, g, b); + } + @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (!surfaceCreated) { 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 f3afcb6a..66e87f83 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -3,11 +3,16 @@ package com.limelight.binding.input; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; +import android.hardware.BatteryState; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.hardware.input.InputManager; +import android.hardware.lights.Light; +import android.hardware.lights.LightState; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightsRequest; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.media.AudioAttributes; @@ -56,6 +61,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD private static final short MAX_GAMEPADS = 16; // Limited by bits in activeGamepadMask + private static final int BATTERY_RECHECK_INTERVAL_MS = 120 * 1000; + private static final Map ANDROID_TO_LI_BUTTON_MAP = Map.ofEntries( Map.entry(KeyEvent.KEYCODE_BUTTON_A, ControllerPacket.A_FLAG), Map.entry(KeyEvent.KEYCODE_BUTTON_B, ControllerPacket.B_FLAG), @@ -867,6 +874,68 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD } } + private static boolean areBatteryCapacitiesEqual(float first, float second) { + // With no NaNs involved, it is a simple equality comparison. + if (!Float.isNaN(first) && !Float.isNaN(second)) { + return first == second; + } + else { + // If we have a NaN in one or both positions, compare NaN-ness instead. + // Equality comparisons will always return false for NaN. + return Float.isNaN(first) == Float.isNaN(second); + } + } + + private void sendControllerBatteryPacket(InputDeviceContext context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + int currentBatteryStatus = context.inputDevice.getBatteryState().getStatus(); + float currentBatteryCapacity = context.inputDevice.getBatteryState().getCapacity(); + + if (currentBatteryStatus != context.lastReportedBatteryStatus || + !areBatteryCapacitiesEqual(currentBatteryCapacity, context.lastReportedBatteryCapacity)) { + byte state; + byte percentage; + + switch (currentBatteryStatus) { + case BatteryState.STATUS_UNKNOWN: + state = MoonBridge.LI_BATTERY_STATE_UNKNOWN; + break; + + case BatteryState.STATUS_CHARGING: + state = MoonBridge.LI_BATTERY_STATE_CHARGING; + break; + + case BatteryState.STATUS_DISCHARGING: + state = MoonBridge.LI_BATTERY_STATE_DISCHARGING; + break; + + case BatteryState.STATUS_NOT_CHARGING: + state = MoonBridge.LI_BATTERY_STATE_NOT_CHARGING; + break; + + case BatteryState.STATUS_FULL: + state = MoonBridge.LI_BATTERY_STATE_FULL; + break; + + default: + return; + } + + if (Float.isNaN(currentBatteryCapacity)) { + percentage = MoonBridge.LI_BATTERY_PERCENTAGE_UNKNOWN; + } + else { + percentage = (byte)(currentBatteryCapacity * 100); + } + + conn.sendControllerBatteryEvent((byte)context.controllerNumber, state, percentage); + + context.lastReportedBatteryStatus = currentBatteryStatus; + context.lastReportedBatteryCapacity = currentBatteryCapacity; + } + } + } + private void sendControllerInputPacket(GenericControllerContext originalContext) { assignControllerNumberIfNeeded(originalContext); @@ -1685,13 +1754,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD for (int i = 0; i < inputDeviceContexts.size(); i++) { InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i); - deviceContext.leftTriggerMotor = leftTrigger; - deviceContext.rightTriggerMotor = rightTrigger; + if (deviceContext.controllerNumber == controllerNumber) { + deviceContext.leftTriggerMotor = leftTrigger; + deviceContext.rightTriggerMotor = rightTrigger; - if (deviceContext.controllerNumber == controllerNumber && deviceContext.quadVibrators) { - rumbleQuadVibrators(deviceContext.vibratorManager, - deviceContext.lowFreqMotor, deviceContext.highFreqMotor, - deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor); + if (deviceContext.quadVibrators) { + rumbleQuadVibrators(deviceContext.vibratorManager, + deviceContext.lowFreqMotor, deviceContext.highFreqMotor, + deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor); + } } } } @@ -1791,6 +1862,36 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD } } + public void handleSetControllerLED(short controllerNumber, byte r, byte g, byte b) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + for (int i = 0; i < inputDeviceContexts.size(); i++) { + InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i); + + if (deviceContext.controllerNumber == controllerNumber) { + // 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()); + } + } + } + } + public boolean handleButtonUp(KeyEvent event) { InputDeviceContext context = getContextForEvent(event); if (context == null) { @@ -2362,6 +2463,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public InputDevice inputDevice; + public LightsManager.LightsSession lightsSession; + + // These are BatteryState values, not Moonlight values + public int lastReportedBatteryStatus; + public float lastReportedBatteryCapacity; + public int leftStickXAxis = -1; public int leftStickYAxis = -1; @@ -2406,6 +2513,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD public long startDownTime = 0; + public final Runnable batteryStateUpdateRunnable = new Runnable() { + @Override + public void run() { + sendControllerBatteryPacket(InputDeviceContext.this); + + // Requeue the callback + handler.postDelayed(this, BATTERY_RECHECK_INTERVAL_MS); + } + }; + @Override public void destroy() { super.destroy(); @@ -2424,7 +2541,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD if (accelListener != null) { inputDevice.getSensorManager().unregisterListener(accelListener); } + + if (lightsSession != null) { + lightsSession.close(); + } } + + handler.removeCallbacks(batteryStateUpdateRunnable); } @Override @@ -2484,6 +2607,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD if (inputDevice.getSensorManager().getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) { capabilities |= MoonBridge.LI_CCAP_GYRO; } + + if (inputDevice.getBatteryState().isPresent()) { + capabilities |= MoonBridge.LI_CCAP_BATTERY_STATE; + } + + for (Light light : inputDevice.getLightsManager().getLights()) { + if (light.hasRgbControl()) { + capabilities |= MoonBridge.LI_CCAP_RGB_LED; + } + } } if (inputDevice.getVibrator().hasVibrator()) { @@ -2499,6 +2632,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD conn.sendControllerArrivalEvent((byte)controllerNumber, getActiveControllerMask(), type, supportedButtonFlags, capabilities); + + // After reporting arrival to the host, send initial battery state and begin monitoring + sendControllerBatteryPacket(this); + handler.postDelayed(batteryStateUpdateRunnable, BATTERY_RECHECK_INTERVAL_MS); } } diff --git a/app/src/main/java/com/limelight/nvstream/NvConnection.java b/app/src/main/java/com/limelight/nvstream/NvConnection.java index 6b4f92d5..6f28fa5b 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnection.java @@ -571,6 +571,10 @@ public class NvConnection { } } + public void sendControllerBatteryEvent(byte controllerNumber, byte batteryState, byte batteryPercentage) { + MoonBridge.sendControllerBatteryEvent(controllerNumber, batteryState, batteryPercentage); + } + public void sendUtf8Text(final String text) { if (!isMonkey) { MoonBridge.sendUtf8Text(text); diff --git a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java index aca0510b..a6109dbf 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java @@ -18,4 +18,6 @@ public interface NvConnectionListener { void setHdrMode(boolean enabled, byte[] hdrMetadata); void setMotionEventState(short controllerNumber, byte motionType, short reportRateHz); + + void setControllerLED(short controllerNumber, byte r, byte g, byte b); } diff --git a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java index 1e907b0c..83f4f9ba 100644 --- a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java +++ b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java @@ -107,10 +107,21 @@ public class MoonBridge { public static final short LI_CCAP_TOUCHPAD = 0x08; public static final short LI_CCAP_ACCEL = 0x10; public static final short LI_CCAP_GYRO = 0x20; + public static final short LI_CCAP_BATTERY_STATE = 0x40; + public static final short LI_CCAP_RGB_LED = 0x80; public static final byte LI_MOTION_TYPE_ACCEL = 0x01; public static final byte LI_MOTION_TYPE_GYRO = 0x02; + public static final byte LI_BATTERY_STATE_UNKNOWN = 0x00; + public static final byte LI_BATTERY_STATE_NOT_PRESENT = 0x01; + public static final byte LI_BATTERY_STATE_DISCHARGING = 0x02; + public static final byte LI_BATTERY_STATE_CHARGING = 0x03; + public static final byte LI_BATTERY_STATE_NOT_CHARGING = 0x04; // Connected to power but not charging + public static final byte LI_BATTERY_STATE_FULL = 0x05; + + public static final byte LI_BATTERY_PERCENTAGE_UNKNOWN = (byte)0xFF; + private static AudioRenderer audioRenderer; private static VideoDecoderRenderer videoRenderer; private static NvConnectionListener connectionListener; @@ -307,6 +318,12 @@ public class MoonBridge { } } + public static void bridgeClSetControllerLED(short controllerNumber, byte r, byte g, byte b) { + if (connectionListener != null) { + connectionListener.setControllerLED(controllerNumber, r, g, b); + } + } + public static void setupBridge(VideoDecoderRenderer videoRenderer, AudioRenderer audioRenderer, NvConnectionListener connectionListener) { MoonBridge.videoRenderer = videoRenderer; MoonBridge.audioRenderer = audioRenderer; @@ -361,6 +378,8 @@ public class MoonBridge { public static native int sendControllerMotionEvent(byte controllerNumber, byte motionType, float x, float y, float z); + public static native int sendControllerBatteryEvent(byte controllerNumber, byte batteryState, byte batteryPercentage); + public static native void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier, byte flags); public static native void sendMouseHighResScroll(short scrollAmount); diff --git a/app/src/main/jni/moonlight-core/callbacks.c b/app/src/main/jni/moonlight-core/callbacks.c index aa770e9b..f0bd76f4 100644 --- a/app/src/main/jni/moonlight-core/callbacks.c +++ b/app/src/main/jni/moonlight-core/callbacks.c @@ -35,6 +35,7 @@ static jmethodID BridgeClConnectionStatusUpdateMethod; static jmethodID BridgeClSetHdrModeMethod; static jmethodID BridgeClRumbleTriggersMethod; static jmethodID BridgeClSetMotionEventStateMethod; +static jmethodID BridgeClSetControllerLEDMethod; static jbyteArray DecodedFrameBuffer; static jshortArray DecodedAudioBuffer; @@ -98,6 +99,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) { BridgeClSetHdrModeMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetHdrMode", "(Z[B)V"); BridgeClRumbleTriggersMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumbleTriggers", "(SSS)V"); BridgeClSetMotionEventStateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetMotionEventState", "(SBS)V"); + BridgeClSetControllerLEDMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetControllerLED", "(SBBB)V"); } int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { @@ -374,6 +376,17 @@ void BridgeClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, } } +void BridgeClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b) { + JNIEnv* env = GetThreadEnv(); + + // These jbyte casts are necessary to satisfy CheckJNI + (*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClSetControllerLEDMethod, controllerNumber, (jbyte)r, (jbyte)g, (jbyte)b); + if ((*env)->ExceptionCheck(env)) { + // We will crash here + (*JVM)->DetachCurrentThread(JVM); + } +} + void BridgeClLogMessage(const char* format, ...) { va_list va; va_start(va, format); @@ -410,6 +423,7 @@ static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = { .setHdrMode = BridgeClSetHdrMode, .rumbleTriggers = BridgeClRumbleTriggers, .setMotionEventState = BridgeClSetMotionEventState, + .setControllerLED = BridgeClSetControllerLED, }; JNIEXPORT jint JNICALL diff --git a/app/src/main/jni/moonlight-core/moonlight-common-c b/app/src/main/jni/moonlight-core/moonlight-common-c index c5dc45e1..c0792168 160000 --- a/app/src/main/jni/moonlight-core/moonlight-common-c +++ b/app/src/main/jni/moonlight-core/moonlight-common-c @@ -1 +1 @@ -Subproject commit c5dc45e1443363d95b9708de26e86ed57b6946e6 +Subproject commit c0792168f5a7ed48fc6feeb7fce01b83df405df2 diff --git a/app/src/main/jni/moonlight-core/simplejni.c b/app/src/main/jni/moonlight-core/simplejni.c index e26ade2f..0b792673 100644 --- a/app/src/main/jni/moonlight-core/simplejni.c +++ b/app/src/main/jni/moonlight-core/simplejni.c @@ -80,6 +80,14 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendControllerMotionEvent(JNIEnv *env return LiSendControllerMotionEvent(controllerNumber, motionType, x, y, z); } +JNIEXPORT jint JNICALL +Java_com_limelight_nvstream_jni_MoonBridge_sendControllerBatteryEvent(JNIEnv *env, jclass clazz, + jbyte controllerNumber, + jbyte batteryState, + jbyte batteryPercentage) { + return LiSendControllerBatteryEvent(controllerNumber, batteryState, batteryPercentage); +} + JNIEXPORT void JNICALL Java_com_limelight_nvstream_jni_MoonBridge_sendKeyboardInput(JNIEnv *env, jclass clazz, jshort keyCode, jbyte keyAction, jbyte modifiers, jbyte flags) { LiSendKeyboardEvent2(keyCode, keyAction, modifiers, flags);