diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java index e0816e408bb8..8ff78ebb0e64 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu; +import android.app.Activity; import android.app.Application; import android.content.Context; import android.hardware.usb.UsbManager; @@ -15,13 +16,15 @@ public class DolphinApplication extends Application { private static DolphinApplication application; + private static ActivityTracker sActivityTracker; @Override public void onCreate() { super.onCreate(); application = this; - registerActivityLifecycleCallbacks(new ActivityTracker()); + sActivityTracker = new ActivityTracker(); + registerActivityLifecycleCallbacks(sActivityTracker); VolleyUtil.init(getApplicationContext()); System.loadLibrary("main"); @@ -36,4 +39,9 @@ public static Context getAppContext() { return application.getApplicationContext(); } + + public static Activity getAppActivity() + { + return sActivityTracker.getCurrentActivity(); + } } 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 482be3dc1647..48034dae41f6 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,10 +2,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui.viewholder -import android.Manifest import android.app.Activity -import android.content.pm.PackageManager -import android.os.Build import android.view.View import android.widget.CompoundButton import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding @@ -63,13 +60,9 @@ class SwitchSettingViewHolder( } if (setting.setting === BooleanSetting.MAIN_EMULATE_WII_SPEAK && isChecked) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - itemView.context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) - != PackageManager.PERMISSION_GRANTED) { - val settingsActivity = itemView.context as Activity - settingsActivity.requestPermissions( - arrayOf(Manifest.permission.RECORD_AUDIO), - PermissionsHandler.REQUEST_CODE_RECORD_AUDIO) + if (!PermissionsHandler.hasRecordAudioPermission(itemView.context)) { + val settingsActivity = itemView.context as Activity + PermissionsHandler.requestRecordAudioPermission(settingsActivity) } } 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 b3a6a5d91a1c..c5bc6cac208b 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 @@ -7,8 +7,12 @@ import android.os.Bundle class ActivityTracker : ActivityLifecycleCallbacks { val resumedActivities = HashSet() var backgroundExecutionAllowed = false + var currentActivity : Activity? = null + private set - override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + currentActivity = activity + } override fun onActivityStarted(activity: Activity) {} @@ -32,7 +36,11 @@ class ActivityTracker : ActivityLifecycleCallbacks { override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} - override fun onActivityDestroyed(activity: Activity) {} + override fun onActivityDestroyed(activity: Activity) { + if (currentActivity === activity) { + currentActivity = null + } + } companion object { @JvmStatic 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 ab68470e2892..527139ada649 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,6 +12,11 @@ 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 { @@ -53,4 +59,32 @@ public static boolean isWritePermissionDenied() { 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 6238c451a9b6..43642aae1e2a 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -927,4 +927,6 @@ It can efficiently compress both junk data and encrypted Wii data. 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 5fe151278f28..7210efbaacce 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -110,6 +110,10 @@ static jclass s_core_device_control_class; static jfieldID s_core_device_control_pointer; static jmethodID s_core_device_control_constructor; +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 @@ -512,6 +516,21 @@ jmethodID GetCoreDeviceControlConstructor() return s_core_device_control_constructor; } +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; @@ -724,6 +743,16 @@ 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 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); @@ -761,5 +790,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_numeric_setting_class); env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_control_class); + env->DeleteGlobalRef(s_permission_handler_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index c324b6cb1987..2aeb2774b8fe 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -109,6 +109,10 @@ jclass GetCoreDeviceControlClass(); jfieldID GetCoreDeviceControlPointer(); jmethodID GetCoreDeviceControlConstructor(); +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 9aeae218abad..0afc89f96cc5 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -20,6 +20,10 @@ #include #endif +#ifdef ANDROID +#include "jni/AndroidCommon/IDCache.h" +#endif + namespace IOS::HLE::USB { Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler) @@ -117,6 +121,19 @@ void Microphone::StreamStart() Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); #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;