diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java index 699e28a9e8..adf882d369 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -11,6 +11,7 @@ import android.os.Build; import android.os.Bundle; import android.util.Pair; import android.util.SparseIntArray; +import android.view.Display; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; @@ -42,6 +43,7 @@ import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding; import org.dolphinemu.dolphinemu.databinding.DialogIrSensitivityBinding; import org.dolphinemu.dolphinemu.databinding.DialogSkylandersManagerBinding; import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface; +import org.dolphinemu.dolphinemu.features.input.model.SensorEventRequester; import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; import org.dolphinemu.dolphinemu.features.settings.model.Settings; @@ -442,12 +444,15 @@ public final class EmulationActivity extends AppCompatActivity implements ThemeP } updateOrientation(); + + ControllerInterface.enableSensorEvents(() -> getWindowManager().getDefaultDisplay()); } @Override protected void onPause() { super.onPause(); + ControllerInterface.disableSensorEvents(); } @Override diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java index 8a7bc8177f..6038afe0f3 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java @@ -64,6 +64,26 @@ public final class ControllerInterface */ public static native boolean dispatchGenericMotionEvent(MotionEvent event); + /** + * {@link DolphinSensorEventListener} calls this for each axis of a received SensorEvent. + */ + public static native void dispatchSensorEvent(String axisName, float value); + + /** + * Enables delivering sensor events to native code. + * + * @param requester The activity or other component which is requesting sensor events to be + * delivered. + */ + public static native void enableSensorEvents(SensorEventRequester requester); + + /** + * Disables delivering sensor events to native code. + * + * Calling this when sensor events are no longer needed will save battery. + */ + public static native void disableSensorEvents(); + /** * Rescans for input devices. */ diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.java new file mode 100644 index 0000000000..f72a97d113 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.java @@ -0,0 +1,361 @@ +package org.dolphinemu.dolphinemu.features.input.model; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.view.Surface; + +import androidx.annotation.Keep; + +import org.dolphinemu.dolphinemu.DolphinApplication; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +public class DolphinSensorEventListener implements SensorEventListener +{ + // Set of three axes. Creates a negative companion to each axis, and corrects for device rotation. + private static final int AXIS_SET_TYPE_DEVICE_COORDINATES = 0; + // Set of three axes. Creates a negative companion to each axis. + private static final int AXIS_SET_TYPE_OTHER_COORDINATES = 1; + + private static class AxisSetDetails + { + public final int firstAxisOfSet; + public final int axisSetType; + + public AxisSetDetails(int firstAxisOfSet, int axisSetType) + { + this.firstAxisOfSet = firstAxisOfSet; + this.axisSetType = axisSetType; + } + } + + private static class SensorDetails + { + public final int sensorType; + public final String[] axisNames; + public final AxisSetDetails[] axisSetDetails; + + public SensorDetails(int sensorType, String[] axisNames, AxisSetDetails[] axisSetDetails) + { + this.sensorType = sensorType; + this.axisNames = axisNames; + this.axisSetDetails = axisSetDetails; + } + } + + private final SensorManager mSensorManager; + + private final HashMap mSensorDetails = new HashMap<>(); + + private SensorEventRequester mRequester = null; + + // The fastest sampling rate Android lets us use without declaring the HIGH_SAMPLING_RATE_SENSORS + // permission is 200 Hz. This is also the sampling rate of a Wii Remote, so it fits us perfectly. + private static final int SAMPLING_PERIOD_US = 1000000 / 200; + + @Keep + public DolphinSensorEventListener() + { + mSensorManager = (SensorManager) + DolphinApplication.getAppContext().getSystemService(Context.SENSOR_SERVICE); + + addSensors(); + } + + private void addSensors() + { + tryAddSensor(Sensor.TYPE_ACCELEROMETER, new String[]{"Accel Right", "Accel Left", + "Accel Forward", "Accel Backward", "Accel Up", "Accel Down"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + + tryAddSensor(Sensor.TYPE_GYROSCOPE, new String[]{"Gyro Pitch Up", "Gyro Pitch Down", + "Gyro Roll Right", "Gyro Roll Left", "Gyro Yaw Left", "Gyro Yaw Right"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + + tryAddSensor(Sensor.TYPE_LIGHT, "Light"); + + tryAddSensor(Sensor.TYPE_PRESSURE, "Pressure"); + + tryAddSensor(Sensor.TYPE_TEMPERATURE, "Device Temperature"); + + tryAddSensor(Sensor.TYPE_PROXIMITY, "Proximity"); + + tryAddSensor(Sensor.TYPE_GRAVITY, new String[]{"Gravity Right", "Gravity Left", + "Gravity Forward", "Gravity Backward", "Gravity Up", "Gravity Down"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + + tryAddSensor(Sensor.TYPE_LINEAR_ACCELERATION, + new String[]{"Linear Acceleration Right", "Linear Acceleration Left", + "Linear Acceleration Forward", "Linear Acceleration Backward", + "Linear Acceleration Up", "Linear Acceleration Down"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + + // The values provided by this sensor can be interpreted as an Euler vector or a quaternion. + // The directions of X and Y are flipped to match the Wii Remote coordinate system. + tryAddSensor(Sensor.TYPE_ROTATION_VECTOR, + new String[]{"Rotation Vector X-", "Rotation Vector X+", "Rotation Vector Y-", + "Rotation Vector Y+", "Rotation Vector Z+", + "Rotation Vector Z-", "Rotation Vector R", "Rotation Vector Heading Accuracy"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + + tryAddSensor(Sensor.TYPE_RELATIVE_HUMIDITY, "Relative Humidity"); + + tryAddSensor(Sensor.TYPE_AMBIENT_TEMPERATURE, "Ambient Temperature"); + + // The values provided by this sensor can be interpreted as an Euler vector or a quaternion. + // The directions of X and Y are flipped to match the Wii Remote coordinate system. + tryAddSensor(Sensor.TYPE_GAME_ROTATION_VECTOR, + new String[]{"Game Rotation Vector X-", "Game Rotation Vector X+", + "Game Rotation Vector Y-", "Game Rotation Vector Y+", "Game Rotation Vector Z+", + "Game Rotation Vector Z-", "Game Rotation Vector R"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + + tryAddSensor(Sensor.TYPE_GYROSCOPE_UNCALIBRATED, + new String[]{"Gyro Uncalibrated Pitch Up", "Gyro Uncalibrated Pitch Down", + "Gyro Uncalibrated Roll Right", "Gyro Uncalibrated Roll Left", + "Gyro Uncalibrated Yaw Left", "Gyro Uncalibrated Yaw Right", + "Gyro Drift Pitch Up", "Gyro Drift Pitch Down", "Gyro Drift Roll Right", + "Gyro Drift Roll Left", "Gyro Drift Yaw Left", "Gyro Drift Yaw Right"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES), + new AxisSetDetails(3, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + + tryAddSensor(Sensor.TYPE_HEART_RATE, "Heart Rate"); + + if (Build.VERSION.SDK_INT >= 24) + { + tryAddSensor(Sensor.TYPE_HEART_BEAT, "Heart Beat"); + } + + if (Build.VERSION.SDK_INT >= 26) + { + tryAddSensor(Sensor.TYPE_ACCELEROMETER_UNCALIBRATED, + new String[]{"Accel Uncalibrated Right", "Accel Uncalibrated Left", + "Accel Uncalibrated Forward", "Accel Uncalibrated Backward", + "Accel Uncalibrated Up", "Accel Uncalibrated Down", + "Accel Bias Right", "Accel Bias Left", "Accel Bias Forward", + "Accel Bias Backward", "Accel Bias Up", "Accel Bias Down"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES), + new AxisSetDetails(3, AXIS_SET_TYPE_DEVICE_COORDINATES)}); + } + + if (Build.VERSION.SDK_INT >= 30) + { + tryAddSensor(Sensor.TYPE_HINGE_ANGLE, "Hinge Angle"); + } + + if (Build.VERSION.SDK_INT >= 33) + { + // The values provided by this sensor can be interpreted as an Euler vector. + // The directions of X and Y are flipped to match the Wii Remote coordinate system. + tryAddSensor(Sensor.TYPE_HEAD_TRACKER, + new String[]{"Head Rotation Vector X-", "Head Rotation Vector X+", + "Head Rotation Vector Y-", "Head Rotation Vector Y+", + "Head Rotation Vector Z+", "Head Rotation Vector Z-", + "Head Pitch Up", "Head Pitch Down", "Head Roll Right", "Head Roll Left", + "Head Yaw Left", "Head Yaw Right"}, + new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_OTHER_COORDINATES), + new AxisSetDetails(3, AXIS_SET_TYPE_OTHER_COORDINATES)}); + + tryAddSensor(Sensor.TYPE_HEADING, new String[]{"Heading", "Heading Accuracy"}, + new AxisSetDetails[]{}); + } + } + + private void tryAddSensor(int sensorType, String axisName) + { + tryAddSensor(sensorType, new String[]{axisName}, new AxisSetDetails[]{}); + } + + private void tryAddSensor(int sensorType, String[] axisNames, AxisSetDetails[] axisSetDetails) + { + Sensor sensor = mSensorManager.getDefaultSensor(sensorType); + if (sensor != null) + { + mSensorDetails.put(sensor, new SensorDetails(sensorType, axisNames, axisSetDetails)); + } + } + + @Override + public void onSensorChanged(SensorEvent sensorEvent) + { + final SensorDetails sensorDetails = mSensorDetails.get(sensorEvent.sensor); + + final float[] values = sensorEvent.values; + final String[] axisNames = sensorDetails.axisNames; + final AxisSetDetails[] axisSetDetails = sensorDetails.axisSetDetails; + + int eventAxisIndex = 0; + int detailsAxisIndex = 0; + int detailsAxisSetIndex = 0; + while (eventAxisIndex < values.length && detailsAxisIndex < axisNames.length) + { + if (detailsAxisSetIndex < axisSetDetails.length && + axisSetDetails[detailsAxisSetIndex].firstAxisOfSet == eventAxisIndex) + { + int rotation = Surface.ROTATION_0; + if (axisSetDetails[detailsAxisSetIndex].axisSetType == AXIS_SET_TYPE_DEVICE_COORDINATES) + { + rotation = mRequester.getDisplay().getRotation(); + } + + float x, y; + switch (rotation) + { + default: + case Surface.ROTATION_0: + x = values[eventAxisIndex]; + y = values[eventAxisIndex + 1]; + break; + case Surface.ROTATION_90: + x = -values[eventAxisIndex + 1]; + y = values[eventAxisIndex]; + break; + case Surface.ROTATION_180: + x = -values[eventAxisIndex]; + y = -values[eventAxisIndex + 1]; + break; + case Surface.ROTATION_270: + x = values[eventAxisIndex + 1]; + y = -values[eventAxisIndex]; + break; + } + + float z = values[eventAxisIndex + 2]; + + ControllerInterface.dispatchSensorEvent(axisNames[detailsAxisIndex], x); + ControllerInterface.dispatchSensorEvent(axisNames[detailsAxisIndex + 1], x); + ControllerInterface.dispatchSensorEvent(axisNames[detailsAxisIndex + 2], y); + ControllerInterface.dispatchSensorEvent(axisNames[detailsAxisIndex + 3], y); + ControllerInterface.dispatchSensorEvent(axisNames[detailsAxisIndex + 4], z); + ControllerInterface.dispatchSensorEvent(axisNames[detailsAxisIndex + 5], z); + + eventAxisIndex += 3; + detailsAxisIndex += 6; + detailsAxisSetIndex++; + } + else + { + ControllerInterface.dispatchSensorEvent(axisNames[detailsAxisIndex], + values[eventAxisIndex]); + + eventAxisIndex++; + detailsAxisIndex++; + } + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) + { + // We don't care about this + } + + /** + * Enables delivering sensor events to native code. + * + * @param requester The activity or other component which is requesting sensor events to be + * delivered. + */ + @Keep + public void enableSensorEvents(SensorEventRequester requester) + { + if (mRequester != null) + { + throw new IllegalStateException("Attempted to enable sensor events when someone else" + + "had already enabled them"); + } + + mRequester = requester; + + for (Sensor sensor : mSensorDetails.keySet()) + { + mSensorManager.registerListener(this, sensor, SAMPLING_PERIOD_US); + } + } + + /** + * Disables delivering sensor events to native code. + * + * Calling this when sensor events are no longer needed will save battery. + */ + @Keep + public void disableSensorEvents() + { + mRequester = null; + + mSensorManager.unregisterListener(this); + } + + @Keep + public String[] getAxisNames() + { + ArrayList axisNames = new ArrayList<>(); + + for (SensorDetails sensorDetails : getSensorDetailsSorted()) + { + Collections.addAll(axisNames, sensorDetails.axisNames); + } + + return axisNames.toArray(new String[]{}); + } + + @Keep + public boolean[] getNegativeAxes() + { + ArrayList negativeAxes = new ArrayList<>(); + + for (SensorDetails sensorDetails : getSensorDetailsSorted()) + { + int eventAxisIndex = 0; + int detailsAxisIndex = 0; + int detailsAxisSetIndex = 0; + while (detailsAxisIndex < sensorDetails.axisNames.length) + { + if (detailsAxisSetIndex < sensorDetails.axisSetDetails.length && + sensorDetails.axisSetDetails[detailsAxisSetIndex].firstAxisOfSet == eventAxisIndex) + { + negativeAxes.add(false); + negativeAxes.add(true); + negativeAxes.add(false); + negativeAxes.add(true); + negativeAxes.add(false); + negativeAxes.add(true); + + eventAxisIndex += 3; + detailsAxisIndex += 6; + detailsAxisSetIndex++; + } + else + { + negativeAxes.add(false); + + eventAxisIndex++; + detailsAxisIndex++; + } + } + } + + boolean[] result = new boolean[negativeAxes.size()]; + for (int i = 0; i < result.length; i++) + { + result[i] = negativeAxes.get(i); + } + + return result; + } + + private List getSensorDetailsSorted() + { + ArrayList sensorDetails = new ArrayList<>(mSensorDetails.values()); + Collections.sort(sensorDetails, Comparator.comparingInt(s -> s.sensorType)); + return sensorDetails; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/SensorEventRequester.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/SensorEventRequester.java new file mode 100644 index 0000000000..83b58015b2 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/SensorEventRequester.java @@ -0,0 +1,16 @@ +package org.dolphinemu.dolphinemu.features.input.model; + +import android.view.Display; + +import androidx.annotation.NonNull; + +public interface SensorEventRequester +{ + /** + * Returns the display the activity is shown on. + * + * This is used for getting the display orientation for rotating the axes of motion events. + */ + @NonNull + Display getDisplay(); +} diff --git a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp index e5baa4e43e..71b99f9ef3 100644 --- a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp @@ -17,6 +17,7 @@ #include #include +#include "Common/Assert.h" #include "Common/Logging/Log.h" #include "Common/StringUtil.h" @@ -62,12 +63,20 @@ jclass s_controller_interface_class; jmethodID s_controller_interface_register_input_device_listener; jmethodID s_controller_interface_unregister_input_device_listener; +jclass s_sensor_event_listener_class; +jmethodID s_sensor_event_listener_constructor; +jmethodID s_sensor_event_listener_enable_sensor_events; +jmethodID s_sensor_event_listener_disable_sensor_events; +jmethodID s_sensor_event_listener_get_axis_names; +jmethodID s_sensor_event_listener_get_negative_axes; + jintArray s_keycodes_array; using Clock = std::chrono::steady_clock; constexpr Clock::duration ACTIVE_INPUT_TIMEOUT = std::chrono::milliseconds(1000); std::unordered_map s_device_id_to_device_qualifier; +ciface::Core::DeviceQualifier s_sensor_device_qualifier; constexpr int MAX_KEYCODE = AKEYCODE_PROFILE_SWITCH; // Up to date as of SDK 31 @@ -460,7 +469,7 @@ public: explicit AndroidKey(int keycode) : AndroidInput(ConstructKeyName(keycode)) {} }; -class AndroidAxis final : public AndroidInput +class AndroidAxis : public AndroidInput { public: AndroidAxis(int source, int axis, bool negative) @@ -468,6 +477,10 @@ public: { } + AndroidAxis(std::string name, bool negative) : AndroidInput(std::move(name)), m_negative(negative) + { + } + ControlState GetState() const override { return m_negative ? -AndroidInput::GetState() : AndroidInput::GetState(); @@ -477,12 +490,21 @@ private: bool m_negative; }; +class AndroidSensorAxis final : public AndroidAxis +{ +public: + AndroidSensorAxis(std::string name, bool negative) : AndroidAxis(std::move(name), negative) {} + + bool IsDetectable() const override { return false; } +}; + class AndroidDevice final : public Core::Device { public: AndroidDevice(JNIEnv* env, jobject input_device) - : m_source(env->CallIntMethod(input_device, s_input_device_get_sources)), - m_controller_number(env->CallIntMethod(input_device, s_input_device_get_controller_number) + : m_sensor_event_listener(nullptr), + m_source(env->CallIntMethod(input_device, s_input_device_get_sources)), + m_controller_number(env->CallIntMethod(input_device, s_input_device_get_controller_number)) { jstring j_name = reinterpret_cast(env->CallObjectMethod(input_device, s_input_device_get_name)); @@ -495,6 +517,19 @@ public: AddAxes(env, input_device); } + // Constructor for the device added by Dolphin to contain sensor inputs + AndroidDevice(JNIEnv* env, std::string name) + : m_sensor_event_listener(AddSensors(env)), m_source(AINPUT_SOURCE_SENSOR), + m_controller_number(0), m_name(std::move(name)) + { + } + + ~AndroidDevice() + { + if (m_sensor_event_listener) + IDCache::GetEnvForThread()->DeleteGlobalRef(m_sensor_event_listener); + } + std::string GetName() const override { return m_name; } std::string GetSource() const override { return "Android"; } @@ -519,6 +554,8 @@ public: return -3; } + jobject GetSensorEventListener() { return m_sensor_event_listener; } + private: void AddKeys(JNIEnv* env, jobject input_device) { @@ -571,6 +608,35 @@ private: env->DeleteLocalRef(motion_ranges_list); } + jobject AddSensors(JNIEnv* env) + { + jobject sensor_event_listener = + env->NewObject(s_sensor_event_listener_class, s_sensor_event_listener_constructor); + + jobjectArray j_axis_names = reinterpret_cast( + env->CallObjectMethod(sensor_event_listener, s_sensor_event_listener_get_axis_names)); + std::vector axis_names = JStringArrayToVector(env, j_axis_names); + env->DeleteLocalRef(j_axis_names); + + jbooleanArray j_negative_axes = reinterpret_cast( + env->CallObjectMethod(sensor_event_listener, s_sensor_event_listener_get_negative_axes)); + jboolean* negative_axes = env->GetBooleanArrayElements(j_negative_axes, nullptr); + + ASSERT(axis_names.size() == env->GetArrayLength(j_negative_axes)); + for (size_t i = 0; i < axis_names.size(); ++i) + AddInput(new AndroidSensorAxis(axis_names[i], negative_axes[i])); + + env->ReleaseBooleanArrayElements(j_negative_axes, negative_axes, 0); + env->DeleteLocalRef(j_negative_axes); + + jobject global_sensor_event_listener = env->NewGlobalRef(sensor_event_listener); + + env->DeleteLocalRef(sensor_event_listener); + + return global_sensor_event_listener; + } + + const jobject m_sensor_event_listener; const int m_source; const int m_controller_number; std::string m_name; @@ -648,6 +714,22 @@ void Init() s_controller_interface_unregister_input_device_listener = env->GetStaticMethodID(s_controller_interface_class, "unregisterInputDeviceListener", "()V"); + const jclass sensor_event_listener_class = + env->FindClass("org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener"); + s_sensor_event_listener_class = + reinterpret_cast(env->NewGlobalRef(sensor_event_listener_class)); + s_sensor_event_listener_constructor = + env->GetMethodID(s_sensor_event_listener_class, "", "()V"); + s_sensor_event_listener_enable_sensor_events = + env->GetMethodID(s_sensor_event_listener_class, "enableSensorEvents", + "(Lorg/dolphinemu/dolphinemu/features/input/model/SensorEventRequester;)V"); + s_sensor_event_listener_disable_sensor_events = + env->GetMethodID(s_sensor_event_listener_class, "disableSensorEvents", "()V"); + s_sensor_event_listener_get_axis_names = + env->GetMethodID(s_sensor_event_listener_class, "getAxisNames", "()[Ljava/lang/String;"); + s_sensor_event_listener_get_negative_axes = + env->GetMethodID(s_sensor_event_listener_class, "getNegativeAxes", "()[Z"); + jintArray keycodes_array = CreateKeyCodesArray(env); s_keycodes_array = reinterpret_cast(env->NewGlobalRef(keycodes_array)); env->DeleteLocalRef(keycodes_array); @@ -669,6 +751,7 @@ void Shutdown() env->DeleteGlobalRef(s_key_event_class); env->DeleteGlobalRef(s_motion_event_class); env->DeleteGlobalRef(s_controller_interface_class); + env->DeleteGlobalRef(s_sensor_event_listener_class); env->DeleteGlobalRef(s_keycodes_array); } @@ -694,6 +777,25 @@ static void AddDevice(JNIEnv* env, int device_id) s_device_id_to_device_qualifier.emplace(device_id, qualifier); } +static void AddSensorDevice(JNIEnv* env) +{ + // Device sensors (accelerometer, etc.) aren't associated with any Android InputDevice. + // Create an otherwise empty Dolphin input device so that they have somewhere to live. + + auto device = std::make_shared(env, "Device Sensors"); + + if (device->Inputs().empty() && device->Outputs().empty()) + return; + + g_controller_interface.AddDevice(device); + + Core::DeviceQualifier qualifier; + qualifier.FromDevice(device.get()); + + INFO_LOG_FMT(CONTROLLERINTERFACE, "Added sensor device as {}", device->GetQualifiedName()); + s_sensor_device_qualifier = qualifier; +} + void PopulateDevices() { INFO_LOG_FMT(CONTROLLERINTERFACE, "Android populating devices"); @@ -708,6 +810,8 @@ void PopulateDevices() AddDevice(env, device_ids[i]); env->ReleaseIntArrayElements(device_ids_array, device_ids, JNI_ABORT); env->DeleteLocalRef(device_ids_array); + + AddSensorDevice(env); } } // namespace ciface::Android @@ -806,6 +910,56 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch return last_polled >= Clock::now() - ACTIVE_INPUT_TIMEOUT; } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchSensorEvent( + JNIEnv* env, jclass, jstring j_axis_name, jfloat value) +{ + const std::shared_ptr device = + g_controller_interface.FindDevice(s_sensor_device_qualifier); + if (!device) + return; + + const std::string axis_name = GetJString(env, j_axis_name); + + for (ciface::Core::Device::Input* input : device->Inputs()) + { + const std::string input_name = input->GetName(); + if (input_name == axis_name) + { + auto casted_input = static_cast(input); + casted_input->SetState(value); + } + } +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_enableSensorEvents( + JNIEnv* env, jclass, jobject j_sensor_event_requester) +{ + const std::shared_ptr device = + g_controller_interface.FindDevice(s_sensor_device_qualifier); + if (!device) + return; + + env->CallVoidMethod( + static_cast(device.get())->GetSensorEventListener(), + s_sensor_event_listener_enable_sensor_events, j_sensor_event_requester); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_disableSensorEvents( + JNIEnv* env, jclass) +{ + const std::shared_ptr device = + g_controller_interface.FindDevice(s_sensor_device_qualifier); + if (!device) + return; + + env->CallVoidMethod( + static_cast(device.get())->GetSensorEventListener(), + s_sensor_event_listener_disable_sensor_events); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_refreshDevices(JNIEnv* env, jclass)