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;