diff options
| author | JMC47 <JMC4789@gmail.com> | 2025-03-28 18:25:04 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-28 18:25:04 -0400 |
| commit | 7d794897c4043b9ee8b278340cfeff02923512f4 (patch) | |
| tree | 05205ca6145a2450d3b3cc5e14526f83f8a379da /Source/Android | |
| parent | e0032b3e2c821f11c1c2964d4901399e88429898 (diff) | |
| parent | 9e9faf3be1582ac5cdcaa46dc60287b10c921aa2 (diff) | |
Merge pull request #13434 from JosJuice/android-non-blocking-input-detection
Android: Don't use separate thread for MotionAlertDialog
Diffstat (limited to 'Source/Android')
9 files changed, 214 insertions, 62 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputDetector.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputDetector.kt new file mode 100644 index 0000000000..e180814a58 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputDetector.kt @@ -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 +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt index 32775a7ebc..622f4f5515 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt @@ -2,23 +2,7 @@ package org.dolphinemu.dolphinemu.features.input.model -import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController - 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( control: String, device: String, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt index ff4a278f4c..12f3ffb138 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt @@ -3,12 +3,16 @@ package org.dolphinemu.dolphinemu.features.input.ui import android.app.Activity +import android.os.Looper +import android.os.Handler import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.dolphinemu.dolphinemu.R 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 /** @@ -24,21 +28,21 @@ class MotionAlertDialog( private val setting: InputMappingControlSetting, private val allDevices: Boolean ) : AlertDialog(activity) { + private val handler = Handler(Looper.getMainLooper()) + private val inputDetector: InputDetector = InputDetector() private var running = false override fun onStart() { super.onStart() - running = true - Thread { - val result = MappingCommon.detectInput(setting.controller, allDevices) - activity.runOnUiThread { - if (running) { - setting.value = result - dismiss() - } - } - }.start() + inputDetector.start(setting.controller.getDefaultDevice(), allDevices) + periodicUpdate() + if (running == false) { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.input_binding_disconnected_device) + .setPositiveButton(R.string.ok, null) + .show() + } } override fun onStop() { @@ -48,9 +52,11 @@ class MotionAlertDialog( override fun dispatchKeyEvent(event: KeyEvent): Boolean { ControllerInterface.dispatchKeyEvent(event) + updateInputDetector() if (event.keyCode == KeyEvent.KEYCODE_BACK && event.isLongPress) { // Special case: Let the user cancel by long-pressing Back (intended for non-touch devices) setting.clearValue() + running = false dismiss() } return true @@ -63,6 +69,29 @@ class MotionAlertDialog( } ControllerInterface.dispatchGenericMotionEvent(event) + updateInputDetector() 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) + } + } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index e10a781114..c0b33237af 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -54,7 +54,8 @@ <string name="input_binding">Input Binding</string> <string name="input_binding_description">Press or move an input to bind it to %1$s.</string> - <string name="input_binding_no_device">You need to select a device first!</string> + <string name="input_binding_no_device">You need to select a device first.</string> + <string name="input_binding_disconnected_device">The selected device is disconnected.\n\nPlease reconnect the device or select a different device.</string> <string name="input_configure_input">Configure Input</string> <string name="input_configure_output">Configure Output</string> <string name="input_expression">Expression</string> diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 33a679bf55..ed382745c0 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -113,6 +113,9 @@ static jclass s_core_device_control_class; static jfieldID s_core_device_control_pointer; static jmethodID s_core_device_control_constructor; +static jclass s_input_detector_class; +static jfieldID s_input_detector_pointer; + static jmethodID s_runnable_run; namespace IDCache @@ -525,6 +528,16 @@ jmethodID GetCoreDeviceControlConstructor() return s_core_device_control_constructor; } +jclass GetInputDetectorClass() +{ + return s_input_detector_class; +} + +jfieldID GetInputDetectorPointer() +{ + return s_input_detector_pointer; +} + jmethodID GetRunnableRun() { 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"); 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"); s_runnable_run = env->GetMethodID(runnable_class, "run", "()V"); 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_group_class); env->DeleteGlobalRef(s_control_reference_class); + env->DeleteGlobalRef(s_control_group_container_class); env->DeleteGlobalRef(s_emulated_controller_class); env->DeleteGlobalRef(s_numeric_setting_class); env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_control_class); - env->DeleteGlobalRef(s_control_group_container_class); + env->DeleteGlobalRef(s_input_detector_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index d28b493a5b..0b01d14b42 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -112,6 +112,9 @@ jclass GetCoreDeviceControlClass(); jfieldID GetCoreDeviceControlPointer(); jmethodID GetCoreDeviceControlConstructor(); +jclass GetInputDetectorClass(); +jfieldID GetInputDetectorPointer(); + jmethodID GetRunnableRun(); } // namespace IDCache diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index 7ddae94c07..be200affa9 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -26,6 +26,7 @@ add_library(main SHARED Input/CoreDevice.h Input/EmulatedController.cpp Input/EmulatedController.h + Input/InputDetector.cpp Input/InputOverrider.cpp Input/MappingCommon.cpp Input/NumericSetting.cpp diff --git a/Source/Android/jni/Input/InputDetector.cpp b/Source/Android/jni/Input/InputDetector.cpp new file mode 100644 index 0000000000..03ad75ba1e --- /dev/null +++ b/Source/Android/jni/Input/InputDetector.cpp @@ -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)); +} +} diff --git a/Source/Android/jni/Input/MappingCommon.cpp b/Source/Android/jni/Input/MappingCommon.cpp index 1cc55df2ae..2017c892db 100644 --- a/Source/Android/jni/Input/MappingCommon.cpp +++ b/Source/Android/jni/Input/MappingCommon.cpp @@ -17,43 +17,10 @@ #include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/ControllerInterface/MappingCommon.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" { 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 Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_getExpressionForControl( JNIEnv* env, jclass, jstring j_control, jstring j_device, jstring j_default_device) { |
