Android: Don't use separate thread for MotionAlertDialog

This is an Android continuation of bc95c00. We now call
InputDetector::Update immediately after receiving an input event from
Android instead of periodically calling it in a sleep loop. This
improves detection of very short inputs, which are especially likely to
occur for volume buttons on phones (or at least on my phone) if you
don't intentionally keep them held down.
This commit is contained in:
JosJuice 2025-03-16 11:05:06 +01:00
parent 1515cf6ccd
commit 516c1314d2
10 changed files with 210 additions and 90 deletions

View file

@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.input.model
import androidx.annotation.Keep
/**
* Waits for the user to press inputs, and reports which inputs were pressed.
*
* The caller is responsible for forwarding input events from Android to ControllerInterface
* and then calling [update].
*/
class InputDetector {
@Keep
private val pointer: Long
constructor() {
pointer = createNew()
}
@Keep
private constructor(pointer: Long) {
this.pointer = pointer
}
external fun finalize()
private external fun createNew(): Long
/**
* Starts a detection session.
*
* @param defaultDevice The device to detect inputs from.
* @param allDevices Whether to also detect inputs from devices other than the specified one.
*/
external fun start(defaultDevice: String, allDevices: Boolean)
/**
* Checks what inputs are currently pressed and updates internal state.
*
* During a detection session, this should be called after each call to
* [ControllerInterface.dispatchKeyEvent] and [ControllerInterface#dispatchGenericMotionEvent].
*/
external fun update()
/**
* Returns whether a detection session has finished.
*
* A detection session can end once the user has pressed and released an input or once a timeout
* has been reached.
*/
external fun isComplete(): Boolean
/**
* Returns the result of a detection session.
*
* The result of each detection session is only returned once. If this method is called more
* than once without starting a new detection session, the second call onwards will return an
* empty string.
*
* @param defaultDevice The device to detect inputs from. Should normally be the same as the one
* passed to [start].
*
* @return The input(s) pressed by the user in the form of an InputCommon expression,
* or an empty string if there were no inputs.
*/
external fun takeResults(defaultDevice: String): String
}

View file

@ -2,23 +2,7 @@
package org.dolphinemu.dolphinemu.features.input.model package org.dolphinemu.dolphinemu.features.input.model
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController
object MappingCommon { object MappingCommon {
/**
* Waits until the user presses one or more inputs or until a timeout,
* then returns the pressed inputs.
*
* When this is being called, a separate thread must be calling ControllerInterface's
* dispatchKeyEvent and dispatchGenericMotionEvent, otherwise no inputs will be registered.
*
* @param controller The device to detect inputs from.
* @param allDevices Whether to also detect inputs from devices other than the specified one.
* @return The input(s) pressed by the user in the form of an InputCommon expression,
* or an empty string if there were no inputs.
*/
external fun detectInput(controller: EmulatedController, allDevices: Boolean): String
external fun getExpressionForControl( external fun getExpressionForControl(
control: String, control: String,
device: String, device: String,

View file

@ -3,12 +3,14 @@
package org.dolphinemu.dolphinemu.features.input.ui package org.dolphinemu.dolphinemu.features.input.ui
import android.app.Activity import android.app.Activity
import android.os.Looper
import android.os.Handler
import android.view.InputDevice import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.MappingCommon import org.dolphinemu.dolphinemu.features.input.model.InputDetector
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting
/** /**
@ -24,21 +26,15 @@ class MotionAlertDialog(
private val setting: InputMappingControlSetting, private val setting: InputMappingControlSetting,
private val allDevices: Boolean private val allDevices: Boolean
) : AlertDialog(activity) { ) : AlertDialog(activity) {
private val handler = Handler(Looper.getMainLooper())
private val inputDetector: InputDetector = InputDetector()
private var running = false private var running = false
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
running = true running = true
Thread { inputDetector.start(setting.controller.getDefaultDevice(), allDevices)
val result = MappingCommon.detectInput(setting.controller, allDevices) periodicUpdate()
activity.runOnUiThread {
if (running) {
setting.value = result
dismiss()
}
}
}.start()
} }
override fun onStop() { override fun onStop() {
@ -48,9 +44,11 @@ class MotionAlertDialog(
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
ControllerInterface.dispatchKeyEvent(event) ControllerInterface.dispatchKeyEvent(event)
updateInputDetector()
if (event.keyCode == KeyEvent.KEYCODE_BACK && event.isLongPress) { if (event.keyCode == KeyEvent.KEYCODE_BACK && event.isLongPress) {
// Special case: Let the user cancel by long-pressing Back (intended for non-touch devices) // Special case: Let the user cancel by long-pressing Back (intended for non-touch devices)
setting.clearValue() setting.clearValue()
running = false
dismiss() dismiss()
} }
return true return true
@ -63,6 +61,29 @@ class MotionAlertDialog(
} }
ControllerInterface.dispatchGenericMotionEvent(event) ControllerInterface.dispatchGenericMotionEvent(event)
updateInputDetector()
return true return true
} }
private fun updateInputDetector() {
if (running) {
if (inputDetector.isComplete()) {
setting.value = inputDetector.takeResults(setting.controller.getDefaultDevice())
running = false
// Quirk: If this method has been called from onStart, calling dismiss directly
// doesn't seem to do anything. As a workaround, post a call to dismiss instead.
handler.post(this::dismiss)
} else {
inputDetector.update()
}
}
}
private fun periodicUpdate() {
updateInputDetector()
if (running) {
handler.postDelayed(this::periodicUpdate, 10)
}
}
} }

View file

@ -113,6 +113,9 @@ static jclass s_core_device_control_class;
static jfieldID s_core_device_control_pointer; static jfieldID s_core_device_control_pointer;
static jmethodID s_core_device_control_constructor; static jmethodID s_core_device_control_constructor;
static jclass s_input_detector_class;
static jfieldID s_input_detector_pointer;
static jmethodID s_runnable_run; static jmethodID s_runnable_run;
namespace IDCache namespace IDCache
@ -525,6 +528,16 @@ jmethodID GetCoreDeviceControlConstructor()
return s_core_device_control_constructor; return s_core_device_control_constructor;
} }
jclass GetInputDetectorClass()
{
return s_input_detector_class;
}
jfieldID GetInputDetectorPointer()
{
return s_input_detector_pointer;
}
jmethodID GetRunnableRun() jmethodID GetRunnableRun()
{ {
return s_runnable_run; return s_runnable_run;
@ -746,6 +759,12 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
"(Lorg/dolphinemu/dolphinemu/features/input/model/CoreDevice;J)V"); "(Lorg/dolphinemu/dolphinemu/features/input/model/CoreDevice;J)V");
env->DeleteLocalRef(core_device_control_class); env->DeleteLocalRef(core_device_control_class);
const jclass input_detector_class =
env->FindClass("org/dolphinemu/dolphinemu/features/input/model/InputDetector");
s_input_detector_class = reinterpret_cast<jclass>(env->NewGlobalRef(input_detector_class));
s_input_detector_pointer = env->GetFieldID(input_detector_class, "pointer", "J");
env->DeleteLocalRef(input_detector_class);
const jclass runnable_class = env->FindClass("java/lang/Runnable"); const jclass runnable_class = env->FindClass("java/lang/Runnable");
s_runnable_run = env->GetMethodID(runnable_class, "run", "()V"); s_runnable_run = env->GetMethodID(runnable_class, "run", "()V");
env->DeleteLocalRef(runnable_class); env->DeleteLocalRef(runnable_class);
@ -779,10 +798,11 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_control_class); env->DeleteGlobalRef(s_control_class);
env->DeleteGlobalRef(s_control_group_class); env->DeleteGlobalRef(s_control_group_class);
env->DeleteGlobalRef(s_control_reference_class); env->DeleteGlobalRef(s_control_reference_class);
env->DeleteGlobalRef(s_control_group_container_class);
env->DeleteGlobalRef(s_emulated_controller_class); env->DeleteGlobalRef(s_emulated_controller_class);
env->DeleteGlobalRef(s_numeric_setting_class); env->DeleteGlobalRef(s_numeric_setting_class);
env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_class);
env->DeleteGlobalRef(s_core_device_control_class); env->DeleteGlobalRef(s_core_device_control_class);
env->DeleteGlobalRef(s_control_group_container_class); env->DeleteGlobalRef(s_input_detector_class);
} }
} }

View file

@ -112,6 +112,9 @@ jclass GetCoreDeviceControlClass();
jfieldID GetCoreDeviceControlPointer(); jfieldID GetCoreDeviceControlPointer();
jmethodID GetCoreDeviceControlConstructor(); jmethodID GetCoreDeviceControlConstructor();
jclass GetInputDetectorClass();
jfieldID GetInputDetectorPointer();
jmethodID GetRunnableRun(); jmethodID GetRunnableRun();
} // namespace IDCache } // namespace IDCache

View file

@ -26,6 +26,7 @@ add_library(main SHARED
Input/CoreDevice.h Input/CoreDevice.h
Input/EmulatedController.cpp Input/EmulatedController.cpp
Input/EmulatedController.h Input/EmulatedController.h
Input/InputDetector.cpp
Input/InputOverrider.cpp Input/InputOverrider.cpp
Input/MappingCommon.cpp Input/MappingCommon.cpp
Input/NumericSetting.cpp Input/NumericSetting.cpp

View file

@ -0,0 +1,79 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <jni.h>
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/ControllerInterface/CoreDevice.h"
#include "InputCommon/ControllerInterface/MappingCommon.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
namespace
{
constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3);
constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(0);
constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5);
} // namespace
static ciface::Core::InputDetector* GetPointer(JNIEnv* env, jobject obj)
{
return reinterpret_cast<ciface::Core::InputDetector*>(
env->GetLongField(obj, IDCache::GetInputDetectorPointer()));
}
extern "C" {
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_finalize(JNIEnv* env, jobject obj)
{
delete GetPointer(env, obj);
}
JNIEXPORT jlong JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_createNew(JNIEnv*, jobject)
{
return reinterpret_cast<jlong>(new ciface::Core::InputDetector);
}
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_start(
JNIEnv* env, jobject obj, jstring j_default_device, jboolean all_devices)
{
std::vector<std::string> device_strings;
if (all_devices)
device_strings = g_controller_interface.GetAllDeviceStrings();
else
device_strings = {GetJString(env, j_default_device)};
GetPointer(env, obj)->Start(g_controller_interface, device_strings);
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_update(JNIEnv* env, jobject obj)
{
GetPointer(env, obj)->Update(INPUT_DETECT_INITIAL_TIME, INPUT_DETECT_CONFIRMATION_TIME,
INPUT_DETECT_MAXIMUM_TIME);
}
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_isComplete(JNIEnv* env,
jobject obj)
{
return GetPointer(env, obj)->IsComplete();
}
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_InputDetector_takeResults(
JNIEnv* env, jobject obj, jstring j_default_device)
{
ciface::Core::DeviceQualifier default_device;
default_device.FromString(GetJString(env, j_default_device));
auto detections = GetPointer(env, obj)->TakeResults();
ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections);
return ToJString(env, ciface::MappingCommon::BuildExpression(detections, default_device,
ciface::MappingCommon::Quote::On));
}
}

View file

@ -17,42 +17,9 @@
#include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/ControllerInterface/MappingCommon.h" #include "InputCommon/ControllerInterface/MappingCommon.h"
#include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/Input/EmulatedController.h"
namespace
{
constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3);
constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(0);
constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5);
} // namespace
extern "C" { extern "C" {
JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_detectInput(
JNIEnv* env, jclass, jobject j_emulated_controller, jboolean all_devices)
{
ControllerEmu::EmulatedController* emulated_controller =
EmulatedControllerFromJava(env, j_emulated_controller);
const ciface::Core::DeviceQualifier default_device = emulated_controller->GetDefaultDevice();
std::vector<std::string> device_strings;
if (all_devices)
device_strings = g_controller_interface.GetAllDeviceStrings();
else
device_strings = {default_device.ToString()};
auto detections =
g_controller_interface.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME,
INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME);
ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections);
return ToJString(env, ciface::MappingCommon::BuildExpression(detections, default_device,
ciface::MappingCommon::Quote::On));
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_getExpressionForControl( Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_getExpressionForControl(
JNIEnv* env, jclass, jstring j_control, jstring j_device, jstring j_default_device) JNIEnv* env, jclass, jstring j_control, jstring j_device, jstring j_default_device)

View file

@ -335,29 +335,6 @@ bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const
return device != nullptr && device->IsValid(); return device != nullptr && device->IsValid();
} }
// Wait for inputs on supplied devices.
// Inputs are only considered if they are first seen in a neutral state.
// This is useful for crazy flightsticks that have certain buttons that are always held down
// and also properly handles detection when using "FullAnalogSurface" inputs.
// Multiple detections are returned until the various timeouts have been reached.
auto DeviceContainer::DetectInput(const std::vector<std::string>& device_strings,
std::chrono::milliseconds initial_wait,
std::chrono::milliseconds confirmation_wait,
std::chrono::milliseconds maximum_wait) const
-> std::vector<InputDetection>
{
InputDetector input_detector;
input_detector.Start(*this, device_strings);
while (!input_detector.IsComplete())
{
Common::SleepCurrentThread(10);
input_detector.Update(initial_wait, confirmation_wait, maximum_wait);
}
return input_detector.TakeResults();
}
struct InputDetector::Impl struct InputDetector::Impl
{ {
struct InputState struct InputState

View file

@ -230,20 +230,20 @@ public:
bool HasConnectedDevice(const DeviceQualifier& qualifier) const; bool HasConnectedDevice(const DeviceQualifier& qualifier) const;
std::vector<InputDetection> DetectInput(const std::vector<std::string>& device_strings,
std::chrono::milliseconds initial_wait,
std::chrono::milliseconds confirmation_wait,
std::chrono::milliseconds maximum_wait) const;
std::recursive_mutex& GetDevicesMutex() const { return m_devices_mutex; } std::recursive_mutex& GetDevicesMutex() const { return m_devices_mutex; }
protected: protected:
// Exclusively needed when reading/writing the "m_devices" array. // Exclusively needed when reading/writing the "m_devices" array.
// Not needed when individually readring/writing a single device ptr. // Not needed when individually reading/writing a single device ptr.
mutable std::recursive_mutex m_devices_mutex; mutable std::recursive_mutex m_devices_mutex;
std::vector<std::shared_ptr<Device>> m_devices; std::vector<std::shared_ptr<Device>> m_devices;
}; };
// Wait for inputs on supplied devices.
// Inputs are only considered if they are first seen in a neutral state.
// This is useful for wacky flight sticks that have certain buttons that are always held down
// and also properly handles detection when using "FullAnalogSurface" inputs.
// Multiple detections are returned until the various timeouts have been reached.
class InputDetector class InputDetector
{ {
public: public: