From 065481d9899e6debc94bdbbb812512757f8eac62 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 17 Sep 2022 12:12:39 +0200 Subject: [PATCH] ControllerInterface/Android: Automatically suspend sensors This is a battery-saving measure. Whether a sensor should be suspended is determined in the same way as whether key events and motion events should be handled by the OS rather than consumed by Dolphin. --- .../activities/EmulationActivity.java | 7 +- .../input/model/ControllerInterface.java | 22 ++-- .../model/DolphinSensorEventListener.java | 110 ++++++++++------ .../input/model/SensorEventRequester.java | 16 --- .../ControllerInterface/Android/Android.cpp | 122 ++++++++++++------ 5 files changed, 164 insertions(+), 113 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/SensorEventRequester.java 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 adf882d369..4a9816c259 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,7 +11,6 @@ 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; @@ -43,7 +42,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.input.model.DolphinSensorEventListener; import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; import org.dolphinemu.dolphinemu.features.settings.model.Settings; @@ -445,14 +444,14 @@ public final class EmulationActivity extends AppCompatActivity implements ThemeP updateOrientation(); - ControllerInterface.enableSensorEvents(() -> getWindowManager().getDefaultDisplay()); + DolphinSensorEventListener.setDeviceRotation( + getWindowManager().getDefaultDisplay().getRotation()); } @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 96480ab69c..2b6e1d62ee 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 @@ -66,24 +66,22 @@ public final class ControllerInterface /** * {@link DolphinSensorEventListener} calls this for each axis of a received SensorEvent. + * + * @return true if the emulator core seems to be interested in this event. + * false if the sensor can be suspended to save battery. */ - public static native void dispatchSensorEvent(String deviceQualifier, String axisName, + public static native boolean dispatchSensorEvent(String deviceQualifier, String axisName, float value); /** - * Enables delivering sensor events to native code. + * Called when a sensor is suspended or unsuspended. * - * @param requester The activity or other component which is requesting sensor events to be - * delivered. + * @param deviceQualifier A string used by native code for uniquely identifying devices. + * @param axisNames The name of all axes for the sensor. + * @param suspended Whether the sensor is now suspended. */ - 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(); + public static native void notifySensorSuspendedState(String deviceQualifier, String[] axisNames, + boolean suspended); /** * 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 index 21d533a430..cfc4f34dd7 100644 --- 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 @@ -12,12 +12,15 @@ import android.view.Surface; import androidx.annotation.Keep; import org.dolphinemu.dolphinemu.DolphinApplication; +import org.dolphinemu.dolphinemu.utils.Log; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Map; public class DolphinSensorEventListener implements SensorEventListener { @@ -43,6 +46,7 @@ public class DolphinSensorEventListener implements SensorEventListener public final int sensorType; public final String[] axisNames; public final AxisSetDetails[] axisSetDetails; + public boolean isSuspended = true; public SensorDetails(int sensorType, String[] axisNames, AxisSetDetails[] axisSetDetails) { @@ -52,6 +56,8 @@ public class DolphinSensorEventListener implements SensorEventListener } } + private static int sDeviceRotation = Surface.ROTATION_0; + private final SensorManager mSensorManager; private final HashMap mSensorDetails = new HashMap<>(); @@ -60,8 +66,6 @@ public class DolphinSensorEventListener implements SensorEventListener private String mDeviceQualifier = ""; - 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; @@ -218,6 +222,7 @@ public class DolphinSensorEventListener implements SensorEventListener int eventAxisIndex = 0; int detailsAxisIndex = 0; int detailsAxisSetIndex = 0; + boolean keepSensorAlive = false; while (eventAxisIndex < values.length && detailsAxisIndex < axisNames.length) { if (detailsAxisSetIndex < axisSetDetails.length && @@ -227,7 +232,7 @@ public class DolphinSensorEventListener implements SensorEventListener if (mRotateCoordinatesForScreenOrientation && axisSetDetails[detailsAxisSetIndex].axisSetType == AXIS_SET_TYPE_DEVICE_COORDINATES) { - rotation = mRequester.getDisplay().getRotation(); + rotation = sDeviceRotation; } float x, y; @@ -254,17 +259,18 @@ public class DolphinSensorEventListener implements SensorEventListener float z = values[eventAxisIndex + 2]; - ControllerInterface.dispatchSensorEvent(mDeviceQualifier, axisNames[detailsAxisIndex], x); - ControllerInterface.dispatchSensorEvent(mDeviceQualifier, axisNames[detailsAxisIndex + 1], - x); - ControllerInterface.dispatchSensorEvent(mDeviceQualifier, axisNames[detailsAxisIndex + 2], - y); - ControllerInterface.dispatchSensorEvent(mDeviceQualifier, axisNames[detailsAxisIndex + 3], - y); - ControllerInterface.dispatchSensorEvent(mDeviceQualifier, axisNames[detailsAxisIndex + 4], - z); - ControllerInterface.dispatchSensorEvent(mDeviceQualifier, axisNames[detailsAxisIndex + 5], - z); + keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, + axisNames[detailsAxisIndex], x); + keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, + axisNames[detailsAxisIndex + 1], x); + keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, + axisNames[detailsAxisIndex + 2], y); + keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, + axisNames[detailsAxisIndex + 3], y); + keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, + axisNames[detailsAxisIndex + 4], z); + keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, + axisNames[detailsAxisIndex + 5], z); eventAxisIndex += 3; detailsAxisIndex += 6; @@ -272,13 +278,18 @@ public class DolphinSensorEventListener implements SensorEventListener } else { - ControllerInterface.dispatchSensorEvent(mDeviceQualifier, axisNames[detailsAxisIndex], - values[eventAxisIndex]); + keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, + axisNames[detailsAxisIndex], values[eventAxisIndex]); eventAxisIndex++; detailsAxisIndex++; } } + + if (!keepSensorAlive) + { + setSensorSuspended(sensorEvent.sensor, sensorDetails, true); + } } @Override @@ -298,44 +309,48 @@ public class DolphinSensorEventListener implements SensorEventListener } /** - * Enables delivering sensor events to native code. + * If a sensor has been suspended to save battery, this unsuspends it. + * If the sensor isn't currently suspended, nothing happens. * - * @param requester The activity or other component which is requesting sensor events to be - * delivered. + * @param axisName The name of any of the sensor's axes. */ @Keep - public void enableSensorEvents(SensorEventRequester requester) + public void requestUnsuspendSensor(String axisName) { - if (mRequester != null) + for (Map.Entry entry : mSensorDetails.entrySet()) { - throw new IllegalStateException("Attempted to enable sensor events when someone else" + - "had already enabled them"); - } - - mRequester = requester; - - if (mSensorManager != null) - { - for (Sensor sensor : mSensorDetails.keySet()) + if (Arrays.asList(entry.getValue().axisNames).contains(axisName)) { - mSensorManager.registerListener(this, sensor, SAMPLING_PERIOD_US); + setSensorSuspended(entry.getKey(), entry.getValue(), false); } } } - /** - * Disables delivering sensor events to native code. - * - * Calling this when sensor events are no longer needed will save battery. - */ - @Keep - public void disableSensorEvents() + private void setSensorSuspended(Sensor sensor, SensorDetails sensorDetails, boolean suspend) { - mRequester = null; + boolean changeOccurred = false; - if (mSensorManager != null) + synchronized (sensorDetails) { - mSensorManager.unregisterListener(this); + if (sensorDetails.isSuspended != suspend) + { + ControllerInterface.notifySensorSuspendedState(mDeviceQualifier, sensorDetails.axisNames, + suspend); + + if (suspend) + mSensorManager.unregisterListener(this, sensor); + else + mSensorManager.registerListener(this, sensor, SAMPLING_PERIOD_US); + + sensorDetails.isSuspended = suspend; + + changeOccurred = true; + } + } + + if (changeOccurred) + { + Log.info((suspend ? "Suspended sensor " : "Unsuspended sensor ") + sensor.getName()); } } @@ -403,4 +418,17 @@ public class DolphinSensorEventListener implements SensorEventListener Collections.sort(sensorDetails, Comparator.comparingInt(s -> s.sensorType)); return sensorDetails; } + + /** + * Should be called when an activity or other component that uses sensor events is resumed. + * + * Sensor events that contain device coordinates will have the coordinates rotated by the value + * passed to this function. + * + * @param deviceRotation The current rotation of the device (i.e. rotation of the default display) + */ + public static void setDeviceRotation(int deviceRotation) + { + sDeviceRotation = deviceRotation; + } } 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 deleted file mode 100644 index 83b58015b2..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/SensorEventRequester.java +++ /dev/null @@ -1,16 +0,0 @@ -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 cf7e602a77..058a9e3171 100644 --- a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp @@ -67,8 +67,7 @@ jclass s_sensor_event_listener_class; jmethodID s_sensor_event_listener_constructor; jmethodID s_sensor_event_listener_constructor_input_device; jmethodID s_sensor_event_listener_set_device_qualifier; -jmethodID s_sensor_event_listener_enable_sensor_events; -jmethodID s_sensor_event_listener_disable_sensor_events; +jmethodID s_sensor_event_listener_request_unsuspend_sensor; jmethodID s_sensor_event_listener_get_axis_names; jmethodID s_sensor_event_listener_get_negative_axes; @@ -494,9 +493,45 @@ private: class AndroidSensorAxis final : public AndroidAxis { public: - AndroidSensorAxis(std::string name, bool negative) : AndroidAxis(std::move(name), negative) {} + // This class does not create its own global reference to the passed-in sensor_event_listener. + // That is, it's up to the device that contains this axis to keep sensor_event_listener valid. + // It does however create its own global reference to the passed-in name. + AndroidSensorAxis(JNIEnv* env, jobject sensor_event_listener, jstring j_name, bool negative) + : AndroidAxis(GetJString(env, j_name), negative), + m_sensor_event_listener(sensor_event_listener), + m_j_name(reinterpret_cast(env->NewGlobalRef(j_name))) + { + } + + ~AndroidSensorAxis() { IDCache::GetEnvForThread()->DeleteGlobalRef(m_j_name); } bool IsDetectable() const override { return false; } + + ControlState GetState() const override + { + if (m_is_suspended.load(std::memory_order_relaxed)) + { + IDCache::GetEnvForThread()->CallVoidMethod( + m_sensor_event_listener, s_sensor_event_listener_request_unsuspend_sensor, m_j_name); + + // m_is_suspended is intentionally not updated here. To prevent the C++ suspended status from + // ending up desynced with the Java suspended status, we only update m_is_suspended when Java + // calls notifySensorSuspendedState (which calls NotifyIsSuspended). This way, Java is the + // authoritative source for the suspended status, and C++ mirrors it (possibly with a delay). + } + + return AndroidAxis::GetState(); + } + + void NotifyIsSuspended(bool is_suspended) + { + m_is_suspended.store(is_suspended, std::memory_order_relaxed); + } + +private: + const jobject m_sensor_event_listener; + const jstring m_j_name; + std::atomic m_is_suspended = true; }; class AndroidDevice final : public Core::Device @@ -611,40 +646,45 @@ private: jobject AddSensors(JNIEnv* env, jobject input_device) { - jobject sensor_event_listener; + jobject local_sensor_event_listener; if (input_device) { - sensor_event_listener = + local_sensor_event_listener = env->NewObject(s_sensor_event_listener_class, s_sensor_event_listener_constructor_input_device, input_device); } else { - sensor_event_listener = + local_sensor_event_listener = env->NewObject(s_sensor_event_listener_class, s_sensor_event_listener_constructor); } + jobject sensor_event_listener = env->NewGlobalRef(local_sensor_event_listener); + + env->DeleteLocalRef(local_sensor_event_listener); + 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])); + const jsize axis_count = env->GetArrayLength(j_axis_names); + ASSERT(axis_count == env->GetArrayLength(j_negative_axes)); + for (jsize i = 0; i < axis_count; ++i) + { + const jstring axis_name = + reinterpret_cast(env->GetObjectArrayElement(j_axis_names, i)); + AddInput(new AndroidSensorAxis(env, sensor_event_listener, axis_name, negative_axes[i])); + env->DeleteLocalRef(axis_name); + } + env->DeleteLocalRef(j_axis_names); 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; + return sensor_event_listener; } const jobject m_sensor_event_listener; @@ -735,11 +775,8 @@ void Init() env->GetMethodID(s_sensor_event_listener_class, "", "(Landroid/view/InputDevice;)V"); s_sensor_event_listener_set_device_qualifier = env->GetMethodID( s_sensor_event_listener_class, "setDeviceQualifier", "(Ljava/lang/String;)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_request_unsuspend_sensor = env->GetMethodID( + s_sensor_event_listener_class, "requestUnsuspendSensor", "(Ljava/lang/String;)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 = @@ -934,7 +971,7 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch return last_polled >= Clock::now() - ACTIVE_INPUT_TIMEOUT; } -JNIEXPORT void JNICALL +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchSensorEvent( JNIEnv* env, jclass, jstring j_device_qualifier, jstring j_axis_name, jfloat value) { @@ -943,10 +980,12 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch const std::shared_ptr device = g_controller_interface.FindDevice(device_qualifier); if (!device) - return; + return JNI_FALSE; const std::string axis_name = GetJString(env, j_axis_name); + Clock::time_point last_polled{}; + for (ciface::Core::Device::Input* input : device->Inputs()) { const std::string input_name = input->GetName(); @@ -954,31 +993,34 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch { auto casted_input = static_cast(input); casted_input->SetState(value); + last_polled = std::max(last_polled, casted_input->GetLastPolled()); } } + + return last_polled >= Clock::now() - ACTIVE_INPUT_TIMEOUT; } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_enableSensorEvents( - JNIEnv* env, jclass, jobject j_sensor_event_requester) +Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_notifySensorSuspendedState( + JNIEnv* env, jclass, jstring j_device_qualifier, jobjectArray j_axis_names, jboolean suspended) { - for (std::shared_ptr& device : g_controller_interface.GetAllDevices()) - { - env->CallVoidMethod( - static_cast(device.get())->GetSensorEventListener(), - s_sensor_event_listener_enable_sensor_events, j_sensor_event_requester); - } -} + ciface::Core::DeviceQualifier device_qualifier; + device_qualifier.FromString(GetJString(env, j_device_qualifier)); + const std::shared_ptr device = + g_controller_interface.FindDevice(device_qualifier); + if (!device) + return; -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_disableSensorEvents( - JNIEnv* env, jclass) -{ - for (std::shared_ptr& device : g_controller_interface.GetAllDevices()) + const std::vector axis_names = JStringArrayToVector(env, j_axis_names); + + for (ciface::Core::Device::Input* input : device->Inputs()) { - env->CallVoidMethod( - static_cast(device.get())->GetSensorEventListener(), - s_sensor_event_listener_disable_sensor_events); + const std::string input_name = input->GetName(); + if (std::find(axis_names.begin(), axis_names.end(), input_name) != axis_names.end()) + { + auto casted_input = static_cast(input); + casted_input->NotifyIsSuspended(static_cast(suspended)); + } } }