From dbc09bfb0d75f939fd29e68d621cb56fff07ba6e Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 12 May 2024 23:29:00 +0400 Subject: [PATCH] Android: Add emulated Wii Speak --- .../Android/app/src/main/AndroidManifest.xml | 3 ++ .../dolphinemu/DolphinApplication.java | 10 +++++- .../features/settings/model/BooleanSetting.kt | 15 +++++++- .../features/settings/ui/SettingsAdapter.kt | 4 +++ .../settings/ui/SettingsFragmentPresenter.kt | 16 +++++++++ .../ui/viewholder/SwitchSettingViewHolder.kt | 9 +++++ .../dolphinemu/utils/ActivityTracker.kt | 7 ++++ .../dolphinemu/utils/PermissionsHandler.java | 35 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 4 +++ Source/Android/jni/AndroidCommon/IDCache.cpp | 30 ++++++++++++++++ Source/Android/jni/AndroidCommon/IDCache.h | 4 +++ .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 18 ++++++++++ 12 files changed, 153 insertions(+), 2 deletions(-) diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 6d33d6158a..a14e13b135 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -28,6 +28,9 @@ + = HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY)) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index bcf68e67ec..6a8132d32d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -15,6 +15,7 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView import com.google.android.material.color.MaterialColors import com.google.android.material.datepicker.CalendarConstraints @@ -59,6 +60,9 @@ class SettingsAdapter( val settings: Settings? get() = fragmentView.settings + val fragmentActivity: FragmentActivity + get() = fragmentView.fragmentActivity + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index e584e0a2ff..91d1aac809 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -896,6 +896,22 @@ class SettingsFragmentPresenter( 0 ) ) + sl.add( + SwitchSetting( + context, + BooleanSetting.MAIN_EMULATE_WII_SPEAK, + R.string.emulate_wii_speak, + 0 + ) + ) + sl.add( + InvertedSwitchSetting( + context, + BooleanSetting.MAIN_WII_SPEAK_CONNECTED, + R.string.disconnect_wii_speak, + 0 + ) + ) } private fun addAdvancedSettings(sl: ArrayList) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index fc575ead48..ddcf1d39b5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui.viewholder +import android.app.Activity import android.view.View import android.widget.CompoundButton import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding @@ -10,6 +11,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.PermissionsHandler import java.io.File import java.util.* @@ -57,6 +59,13 @@ class SwitchSettingViewHolder( binding.settingSwitch.isEnabled = false } + if (setting.setting === BooleanSetting.MAIN_EMULATE_WII_SPEAK && isChecked) { + if (!PermissionsHandler.hasRecordAudioPermission(itemView.context)) { + val currentActivity = adapter.fragmentActivity as Activity + PermissionsHandler.requestRecordAudioPermission(currentActivity) + } + } + adapter.onBooleanClick(setting, binding.settingSwitch.isChecked) setStyle(binding.textSettingName, setting) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt index f4423c6d36..8bcd1bdecb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt @@ -9,12 +9,15 @@ class ActivityTracker : ActivityLifecycleCallbacks { private val resumedActivities = HashSet() private var backgroundExecutionAllowed = false private var firstStart = true + var currentActivity : Activity? = null + private set private fun isMainActivity(activity: Activity): Boolean { return activity is MainView } override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + currentActivity = activity if (isMainActivity(activity)) { firstStart = bundle == null } @@ -26,6 +29,7 @@ class ActivityTracker : ActivityLifecycleCallbacks { } override fun onActivityResumed(activity: Activity) { + currentActivity = activity resumedActivities.add(activity) if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) { backgroundExecutionAllowed = true @@ -34,6 +38,9 @@ class ActivityTracker : ActivityLifecycleCallbacks { } override fun onActivityPaused(activity: Activity) { + if (currentActivity === activity) { + currentActivity = null + } resumedActivities.remove(activity) if (backgroundExecutionAllowed && resumedActivities.isEmpty()) { backgroundExecutionAllowed = false diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java index a4c69281f2..527139ada6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.utils; +import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; @@ -11,10 +12,16 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static android.Manifest.permission.RECORD_AUDIO; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.DolphinApplication; +import org.dolphinemu.dolphinemu.NativeLibrary; public class PermissionsHandler { public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + public static final int REQUEST_CODE_RECORD_AUDIO = 501; private static boolean sWritePermissionDenied = false; public static void requestWritePermission(final FragmentActivity activity) @@ -52,4 +59,32 @@ public class PermissionsHandler { return sWritePermissionDenied; } + + public static boolean hasRecordAudioPermission(Context context) + { + if (context == null) + context = DolphinApplication.getAppContext(); + int hasRecordPermission = ContextCompat.checkSelfPermission(context, RECORD_AUDIO); + return hasRecordPermission == PackageManager.PERMISSION_GRANTED; + } + + public static void requestRecordAudioPermission(Activity activity) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return; + + if (activity == null) + { + // Calling from C++ code + activity = DolphinApplication.getAppActivity(); + // Since the emulation (and cubeb) has already started, enabling the microphone permission + // now might require restarting the game to be effective. Warn the user about it. + NativeLibrary.displayAlertMsg( + activity.getString(R.string.wii_speak_permission_warning), + activity.getString(R.string.wii_speak_permission_warning_description), + false, true, false); + } + + activity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_RECORD_AUDIO); + } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 22cd5c4a34..dd3e1dcb31 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -944,4 +944,8 @@ It can efficiently compress both junk data and encrypted Wii data. Incompatible Figure Selected Please select a compatible figure file + Wii Speak + Mute Wii Speak + Missing Microphone Permission + Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective. diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index ed382745c0..08ec2c9804 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -116,6 +116,10 @@ static jmethodID s_core_device_control_constructor; static jclass s_input_detector_class; static jfieldID s_input_detector_pointer; +static jclass s_permission_handler_class; +static jmethodID s_permission_handler_has_record_audio_permission; +static jmethodID s_permission_handler_request_record_audio_permission; + static jmethodID s_runnable_run; namespace IDCache @@ -538,6 +542,21 @@ jfieldID GetInputDetectorPointer() return s_input_detector_pointer; } +jclass GetPermissionHandlerClass() +{ + return s_permission_handler_class; +} + +jmethodID GetPermissionHandlerHasRecordAudioPermission() +{ + return s_permission_handler_has_record_audio_permission; +} + +jmethodID GetPermissionHandlerRequestRecordAudioPermission() +{ + return s_permission_handler_request_record_audio_permission; +} + jmethodID GetRunnableRun() { return s_runnable_run; @@ -765,6 +784,16 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_input_detector_pointer = env->GetFieldID(input_detector_class, "pointer", "J"); env->DeleteLocalRef(input_detector_class); + const jclass permission_handler_class = + env->FindClass("org/dolphinemu/dolphinemu/utils/PermissionsHandler"); + s_permission_handler_class = + reinterpret_cast(env->NewGlobalRef(permission_handler_class)); + s_permission_handler_has_record_audio_permission = env->GetStaticMethodID( + permission_handler_class, "hasRecordAudioPermission", "(Landroid/content/Context;)Z"); + s_permission_handler_request_record_audio_permission = env->GetStaticMethodID( + permission_handler_class, "requestRecordAudioPermission", "(Landroid/app/Activity;)V"); + env->DeleteLocalRef(permission_handler_class); + const jclass runnable_class = env->FindClass("java/lang/Runnable"); s_runnable_run = env->GetMethodID(runnable_class, "run", "()V"); env->DeleteLocalRef(runnable_class); @@ -804,5 +833,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_control_class); env->DeleteGlobalRef(s_input_detector_class); + env->DeleteGlobalRef(s_permission_handler_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0b01d14b42..3cc8c11b5e 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -115,6 +115,10 @@ jmethodID GetCoreDeviceControlConstructor(); jclass GetInputDetectorClass(); jfieldID GetInputDetectorPointer(); +jclass GetPermissionHandlerClass(); +jmethodID GetPermissionHandlerHasRecordAudioPermission(); +jmethodID GetPermissionHandlerRequestRecordAudioPermission(); + jmethodID GetRunnableRun(); } // namespace IDCache diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 450247b12b..6e2f40072c 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -23,6 +23,10 @@ #include #endif +#ifdef ANDROID +#include "jni/AndroidCommon/IDCache.h" +#endif + namespace IOS::HLE::USB { Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler) @@ -122,6 +126,20 @@ void Microphone::StreamStart() return; m_work_queue.PushBlocking([this] { #endif + +#ifdef ANDROID + JNIEnv* env = IDCache::GetEnvForThread(); + if (jboolean result = env->CallStaticBooleanMethod( + IDCache::GetPermissionHandlerClass(), + IDCache::GetPermissionHandlerHasRecordAudioPermission(), nullptr); + result == JNI_FALSE) + { + env->CallStaticVoidMethod(IDCache::GetPermissionHandlerClass(), + IDCache::GetPermissionHandlerRequestRecordAudioPermission(), + nullptr); + } +#endif + cubeb_stream_params params{}; params.format = CUBEB_SAMPLE_S16LE; params.rate = SAMPLING_RATE;