mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-06-28 09:59:32 -06:00
Android: Add emulated Wii Speak
This commit is contained in:
@ -28,6 +28,9 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"
|
android:name="android.permission.VIBRATE"
|
||||||
android:required="false"/>
|
android:required="false"/>
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.RECORD_AUDIO"
|
||||||
|
android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".DolphinApplication"
|
android:name=".DolphinApplication"
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package org.dolphinemu.dolphinemu;
|
package org.dolphinemu.dolphinemu;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.hardware.usb.UsbManager;
|
import android.hardware.usb.UsbManager;
|
||||||
@ -15,13 +16,15 @@ import org.dolphinemu.dolphinemu.utils.VolleyUtil;
|
|||||||
public class DolphinApplication extends Application
|
public class DolphinApplication extends Application
|
||||||
{
|
{
|
||||||
private static DolphinApplication application;
|
private static DolphinApplication application;
|
||||||
|
private static ActivityTracker sActivityTracker;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate()
|
public void onCreate()
|
||||||
{
|
{
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
application = this;
|
application = this;
|
||||||
registerActivityLifecycleCallbacks(new ActivityTracker());
|
sActivityTracker = new ActivityTracker();
|
||||||
|
registerActivityLifecycleCallbacks(sActivityTracker);
|
||||||
VolleyUtil.init(getApplicationContext());
|
VolleyUtil.init(getApplicationContext());
|
||||||
System.loadLibrary("main");
|
System.loadLibrary("main");
|
||||||
|
|
||||||
@ -36,4 +39,9 @@ public class DolphinApplication extends Application
|
|||||||
{
|
{
|
||||||
return application.getApplicationContext();
|
return application.getApplicationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Activity getAppActivity()
|
||||||
|
{
|
||||||
|
return sActivityTracker.getCurrentActivity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,6 +238,18 @@ enum class BooleanSetting(
|
|||||||
"EmulateInfinityBase",
|
"EmulateInfinityBase",
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
MAIN_EMULATE_WII_SPEAK(
|
||||||
|
Settings.FILE_DOLPHIN,
|
||||||
|
Settings.SECTION_EMULATED_USB_DEVICES,
|
||||||
|
"EmulateWiiSpeak",
|
||||||
|
false
|
||||||
|
),
|
||||||
|
MAIN_WII_SPEAK_CONNECTED(
|
||||||
|
Settings.FILE_DOLPHIN,
|
||||||
|
Settings.SECTION_EMULATED_USB_DEVICES,
|
||||||
|
"WiiSpeakConnected",
|
||||||
|
false
|
||||||
|
),
|
||||||
MAIN_SHOW_GAME_TITLES(
|
MAIN_SHOW_GAME_TITLES(
|
||||||
Settings.FILE_DOLPHIN,
|
Settings.FILE_DOLPHIN,
|
||||||
Settings.SECTION_INI_ANDROID,
|
Settings.SECTION_INI_ANDROID,
|
||||||
@ -924,7 +936,8 @@ enum class BooleanSetting(
|
|||||||
MAIN_DSP_JIT,
|
MAIN_DSP_JIT,
|
||||||
MAIN_TIME_TRACKING,
|
MAIN_TIME_TRACKING,
|
||||||
MAIN_EMULATE_SKYLANDER_PORTAL,
|
MAIN_EMULATE_SKYLANDER_PORTAL,
|
||||||
MAIN_EMULATE_INFINITY_BASE
|
MAIN_EMULATE_INFINITY_BASE,
|
||||||
|
MAIN_EMULATE_WII_SPEAK
|
||||||
)
|
)
|
||||||
private val NOT_RUNTIME_EDITABLE: Set<BooleanSetting> =
|
private val NOT_RUNTIME_EDITABLE: Set<BooleanSetting> =
|
||||||
HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY))
|
HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY))
|
||||||
|
@ -15,6 +15,7 @@ import android.widget.TextView
|
|||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.datepicker.CalendarConstraints
|
import com.google.android.material.datepicker.CalendarConstraints
|
||||||
@ -59,6 +60,9 @@ class SettingsAdapter(
|
|||||||
val settings: Settings?
|
val settings: Settings?
|
||||||
get() = fragmentView.settings
|
get() = fragmentView.settings
|
||||||
|
|
||||||
|
val fragmentActivity: FragmentActivity
|
||||||
|
get() = fragmentView.fragmentActivity
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
|
@ -896,6 +896,22 @@ class SettingsFragmentPresenter(
|
|||||||
0
|
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<SettingsItem>) {
|
private fun addAdvancedSettings(sl: ArrayList<SettingsItem>) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
|
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding
|
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.model.view.SwitchSetting
|
||||||
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
|
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
|
||||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
||||||
|
import org.dolphinemu.dolphinemu.utils.PermissionsHandler
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -57,6 +59,13 @@ class SwitchSettingViewHolder(
|
|||||||
binding.settingSwitch.isEnabled = false
|
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)
|
adapter.onBooleanClick(setting, binding.settingSwitch.isChecked)
|
||||||
|
|
||||||
setStyle(binding.textSettingName, setting)
|
setStyle(binding.textSettingName, setting)
|
||||||
|
@ -9,12 +9,15 @@ class ActivityTracker : ActivityLifecycleCallbacks {
|
|||||||
private val resumedActivities = HashSet<Activity>()
|
private val resumedActivities = HashSet<Activity>()
|
||||||
private var backgroundExecutionAllowed = false
|
private var backgroundExecutionAllowed = false
|
||||||
private var firstStart = true
|
private var firstStart = true
|
||||||
|
var currentActivity : Activity? = null
|
||||||
|
private set
|
||||||
|
|
||||||
private fun isMainActivity(activity: Activity): Boolean {
|
private fun isMainActivity(activity: Activity): Boolean {
|
||||||
return activity is MainView
|
return activity is MainView
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
|
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
|
||||||
|
currentActivity = activity
|
||||||
if (isMainActivity(activity)) {
|
if (isMainActivity(activity)) {
|
||||||
firstStart = bundle == null
|
firstStart = bundle == null
|
||||||
}
|
}
|
||||||
@ -26,6 +29,7 @@ class ActivityTracker : ActivityLifecycleCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
|
currentActivity = activity
|
||||||
resumedActivities.add(activity)
|
resumedActivities.add(activity)
|
||||||
if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) {
|
if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) {
|
||||||
backgroundExecutionAllowed = true
|
backgroundExecutionAllowed = true
|
||||||
@ -34,6 +38,9 @@ class ActivityTracker : ActivityLifecycleCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) {
|
override fun onActivityPaused(activity: Activity) {
|
||||||
|
if (currentActivity === activity) {
|
||||||
|
currentActivity = null
|
||||||
|
}
|
||||||
resumedActivities.remove(activity)
|
resumedActivities.remove(activity)
|
||||||
if (backgroundExecutionAllowed && resumedActivities.isEmpty()) {
|
if (backgroundExecutionAllowed && resumedActivities.isEmpty()) {
|
||||||
backgroundExecutionAllowed = false
|
backgroundExecutionAllowed = false
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package org.dolphinemu.dolphinemu.utils;
|
package org.dolphinemu.dolphinemu.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@ -11,10 +12,16 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
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 class PermissionsHandler
|
||||||
{
|
{
|
||||||
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
|
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
|
||||||
|
public static final int REQUEST_CODE_RECORD_AUDIO = 501;
|
||||||
private static boolean sWritePermissionDenied = false;
|
private static boolean sWritePermissionDenied = false;
|
||||||
|
|
||||||
public static void requestWritePermission(final FragmentActivity activity)
|
public static void requestWritePermission(final FragmentActivity activity)
|
||||||
@ -52,4 +59,32 @@ public class PermissionsHandler
|
|||||||
{
|
{
|
||||||
return sWritePermissionDenied;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -944,4 +944,8 @@ It can efficiently compress both junk data and encrypted Wii data.
|
|||||||
<string name="incompatible_figure_selected">Incompatible Figure Selected</string>
|
<string name="incompatible_figure_selected">Incompatible Figure Selected</string>
|
||||||
<string name="select_compatible_figure">Please select a compatible figure file</string>
|
<string name="select_compatible_figure">Please select a compatible figure file</string>
|
||||||
|
|
||||||
|
<string name="emulate_wii_speak">Wii Speak</string>
|
||||||
|
<string name="disconnect_wii_speak">Mute Wii Speak</string>
|
||||||
|
<string name="wii_speak_permission_warning">Missing Microphone Permission</string>
|
||||||
|
<string name="wii_speak_permission_warning_description">Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -116,6 +116,10 @@ static jmethodID s_core_device_control_constructor;
|
|||||||
static jclass s_input_detector_class;
|
static jclass s_input_detector_class;
|
||||||
static jfieldID s_input_detector_pointer;
|
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;
|
static jmethodID s_runnable_run;
|
||||||
|
|
||||||
namespace IDCache
|
namespace IDCache
|
||||||
@ -538,6 +542,21 @@ jfieldID GetInputDetectorPointer()
|
|||||||
return s_input_detector_pointer;
|
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()
|
jmethodID GetRunnableRun()
|
||||||
{
|
{
|
||||||
return s_runnable_run;
|
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");
|
s_input_detector_pointer = env->GetFieldID(input_detector_class, "pointer", "J");
|
||||||
env->DeleteLocalRef(input_detector_class);
|
env->DeleteLocalRef(input_detector_class);
|
||||||
|
|
||||||
|
const jclass permission_handler_class =
|
||||||
|
env->FindClass("org/dolphinemu/dolphinemu/utils/PermissionsHandler");
|
||||||
|
s_permission_handler_class =
|
||||||
|
reinterpret_cast<jclass>(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");
|
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);
|
||||||
@ -804,5 +833,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
|
|||||||
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_input_detector_class);
|
env->DeleteGlobalRef(s_input_detector_class);
|
||||||
|
env->DeleteGlobalRef(s_permission_handler_class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,10 @@ jmethodID GetCoreDeviceControlConstructor();
|
|||||||
jclass GetInputDetectorClass();
|
jclass GetInputDetectorClass();
|
||||||
jfieldID GetInputDetectorPointer();
|
jfieldID GetInputDetectorPointer();
|
||||||
|
|
||||||
|
jclass GetPermissionHandlerClass();
|
||||||
|
jmethodID GetPermissionHandlerHasRecordAudioPermission();
|
||||||
|
jmethodID GetPermissionHandlerRequestRecordAudioPermission();
|
||||||
|
|
||||||
jmethodID GetRunnableRun();
|
jmethodID GetRunnableRun();
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
@ -23,6 +23,10 @@
|
|||||||
#include <Objbase.h>
|
#include <Objbase.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ANDROID
|
||||||
|
#include "jni/AndroidCommon/IDCache.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace IOS::HLE::USB
|
namespace IOS::HLE::USB
|
||||||
{
|
{
|
||||||
Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler)
|
Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler)
|
||||||
@ -122,6 +126,20 @@ void Microphone::StreamStart()
|
|||||||
return;
|
return;
|
||||||
m_work_queue.PushBlocking([this] {
|
m_work_queue.PushBlocking([this] {
|
||||||
#endif
|
#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{};
|
cubeb_stream_params params{};
|
||||||
params.format = CUBEB_SAMPLE_S16LE;
|
params.format = CUBEB_SAMPLE_S16LE;
|
||||||
params.rate = SAMPLING_RATE;
|
params.rate = SAMPLING_RATE;
|
||||||
|
Reference in New Issue
Block a user