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.
This commit is contained in:
JosJuice 2022-09-17 12:12:39 +02:00
parent 36acb17700
commit 065481d989
5 changed files with 164 additions and 113 deletions

View File

@ -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

View File

@ -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.

View File

@ -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<Sensor, SensorDetails> 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<Sensor, SensorDetails> entry : mSensorDetails.entrySet())
{
throw new IllegalStateException("Attempted to enable sensor events when someone else" +
"had already enabled them");
if (Arrays.asList(entry.getValue().axisNames).contains(axisName))
{
setSensorSuspended(entry.getKey(), entry.getValue(), false);
}
}
}
mRequester = requester;
private void setSensorSuspended(Sensor sensor, SensorDetails sensorDetails, boolean suspend)
{
boolean changeOccurred = false;
if (mSensorManager != null)
synchronized (sensorDetails)
{
for (Sensor sensor : mSensorDetails.keySet())
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;
}
}
/**
* Disables delivering sensor events to native code.
*
* Calling this when sensor events are no longer needed will save battery.
*/
@Keep
public void disableSensorEvents()
if (changeOccurred)
{
mRequester = null;
if (mSensorManager != null)
{
mSensorManager.unregisterListener(this);
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;
}
}

View File

@ -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();
}

View File

@ -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<jstring>(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<bool> 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<jobjectArray>(
env->CallObjectMethod(sensor_event_listener, s_sensor_event_listener_get_axis_names));
std::vector<std::string> axis_names = JStringArrayToVector(env, j_axis_names);
env->DeleteLocalRef(j_axis_names);
jbooleanArray j_negative_axes = reinterpret_cast<jbooleanArray>(
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<jstring>(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, "<init>", "(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<ciface::Core::Device> 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<ciface::Android::AndroidInput*>(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<ciface::Core::Device>& device : g_controller_interface.GetAllDevices())
{
env->CallVoidMethod(
static_cast<ciface::Android::AndroidDevice*>(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<ciface::Core::Device> 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<ciface::Core::Device>& device : g_controller_interface.GetAllDevices())
const std::vector<std::string> axis_names = JStringArrayToVector(env, j_axis_names);
for (ciface::Core::Device::Input* input : device->Inputs())
{
env->CallVoidMethod(
static_cast<ciface::Android::AndroidDevice*>(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<ciface::Android::AndroidSensorAxis*>(input);
casted_input->NotifyIsSuspended(static_cast<bool>(suspended));
}
}
}