From d11dbac7448a4e83b95a2bd5ee14eb6321a9c770 Mon Sep 17 00:00:00 2001 From: NullVector Date: Sat, 5 Sep 2026 20:00:58 +0930 Subject: [PATCH] Android: get the application to initialize the c++ code and be able to set game directory (#198) --- CMakeLists.txt | 3 + src/android/app/build.gradle.kts | 4 + src/android/app/src/main/AndroidManifest.xml | 8 +- src/android/app/src/main/assets/index.html | 314 +++++++++++++++++- .../app/src/main/assets/nxlogo-32x32.png | Bin 0 -> 2884 bytes .../src/main/java/org/nxemu/NativeLibrary.kt | 63 ++++ .../main/java/org/nxemu/NxEmuApplication.kt | 24 ++ .../java/org/nxemu/SettingsIdentifiers.kt | 12 + .../java/org/nxemu/ui/main/MainActivity.kt | 72 +++- .../java/org/nxemu/ui/main/NxEmuBridge.kt | 26 ++ .../java/org/nxemu/utils/AndroidDevice.kt | 21 ++ .../java/org/nxemu/utils/GpuDriverHelper.kt | 58 ++++ src/android/app/src/main/jni/CMakeLists.txt | 19 ++ src/android/app/src/main/jni/native.cpp | 92 +++++ src/android/app/src/main/jni/notification.cpp | 43 +++ src/android/app/src/main/jni/notification.h | 21 ++ .../src/main/jni/settings/settings_bridge.cpp | 92 +++++ .../src/main/jni/settings/settings_bridge.h | 5 + .../src/main/jni/settings/ui_identifiers.h | 7 + .../app/src/main/jni/settings/ui_settings.cpp | 192 +++++++++++ .../app/src/main/jni/settings/ui_settings.h | 15 + 21 files changed, 1072 insertions(+), 19 deletions(-) create mode 100644 src/android/app/src/main/assets/nxlogo-32x32.png create mode 100644 src/android/app/src/main/java/org/nxemu/NativeLibrary.kt create mode 100644 src/android/app/src/main/java/org/nxemu/NxEmuApplication.kt create mode 100644 src/android/app/src/main/java/org/nxemu/SettingsIdentifiers.kt create mode 100644 src/android/app/src/main/java/org/nxemu/ui/main/NxEmuBridge.kt create mode 100644 src/android/app/src/main/java/org/nxemu/utils/AndroidDevice.kt create mode 100644 src/android/app/src/main/java/org/nxemu/utils/GpuDriverHelper.kt create mode 100644 src/android/app/src/main/jni/CMakeLists.txt create mode 100644 src/android/app/src/main/jni/native.cpp create mode 100644 src/android/app/src/main/jni/notification.cpp create mode 100644 src/android/app/src/main/jni/notification.h create mode 100644 src/android/app/src/main/jni/settings/settings_bridge.cpp create mode 100644 src/android/app/src/main/jni/settings/settings_bridge.h create mode 100644 src/android/app/src/main/jni/settings/ui_identifiers.h create mode 100644 src/android/app/src/main/jni/settings/ui_settings.cpp create mode 100644 src/android/app/src/main/jni/settings/ui_settings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 78a05479..2d044bc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,9 @@ add_subdirectory(src/nxemu-cpu) add_subdirectory(src/nxemu-loader) add_subdirectory(src/nxemu-os) add_subdirectory(src/nxemu-video) +if(ANDROID) + add_subdirectory(src/android/app/src/main/jni) +endif() if(NOT ANDROID) add_subdirectory(src/nxemu) endif() diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 1de1d389..8c81dfb5 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -47,3 +47,7 @@ android { } } } + +dependencies { + implementation("androidx.activity:activity-ktx:1.9.3") +} diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index ad8fc0fc..b001dcf7 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -2,14 +2,20 @@ + + diff --git a/src/android/app/src/main/assets/index.html b/src/android/app/src/main/assets/index.html index 3c9116f4..e9f5fb6b 100644 --- a/src/android/app/src/main/assets/index.html +++ b/src/android/app/src/main/assets/index.html @@ -1,20 +1,308 @@ - + - - + + + -Hello from NxEmu + +
+
+ + +
+
game list
+
+ + + diff --git a/src/android/app/src/main/assets/nxlogo-32x32.png b/src/android/app/src/main/assets/nxlogo-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..eb95a89d5b196d2c69590811221da3f5c20f49cd GIT binary patch literal 2884 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 iT&@1D=Gi}mwf`SxFr%ew_d$990000 Unit)? = null + + @JvmStatic + fun onSettingChanged(setting: String) { + Log.d("NxEmu", "onSettingChanged (kotlin): $setting") + onSettingChangedListener?.invoke(setting) + } + + @JvmStatic + fun restorePersistedGameDirectoryAccess() { + val persisted = NxEmuApplication.appContext.contentResolver.persistedUriPermissions + .map { it.uri.toString() } + .toSet() + + val existing = getSettingString(NXUISetting.GameDirectories) + val dirs = try { + JSONArray(existing) + } catch (_: Exception) { + JSONArray() + } + val kept = JSONArray() + var changed = false + for (i in 0 until dirs.length()) { + val path = dirs.getString(i) + val isContent = path.startsWith("content:", ignoreCase = true) + if (!isContent || persisted.contains(path)) { + kept.put(path) + } else { + changed = true + Log.d("NxEmu", "Dropped game directory without persisted access: $path") + } + } + if (changed) { + setSettingString(NXUISetting.GameDirectories, kept.toString()) + saveSettings() + } + } + + @JvmStatic + external fun appInit(appDirectory: String, nativeModuleLibDir: String) + + @JvmStatic + external fun appCleanup() + + @JvmStatic + external fun getSettingString(setting: String): String + + @JvmStatic + external fun setSettingString(setting: String, value: String) + + @JvmStatic + external fun saveSettings() +} diff --git a/src/android/app/src/main/java/org/nxemu/NxEmuApplication.kt b/src/android/app/src/main/java/org/nxemu/NxEmuApplication.kt new file mode 100644 index 00000000..8e7e2dc8 --- /dev/null +++ b/src/android/app/src/main/java/org/nxemu/NxEmuApplication.kt @@ -0,0 +1,24 @@ +package org.nxemu + +import android.app.Application +import org.nxemu.utils.AndroidDevice +import org.nxemu.utils.GpuDriverHelper + +class NxEmuApplication : Application() { + companion object { + lateinit var appContext: Application + private set + } + + override fun onCreate() { + super.onCreate() + appContext = this + + NativeLibrary.appInit( + AndroidDevice.PACKAGE_DIRECTORY, + AndroidDevice.nativeModuleLibsDirectory(this), + ) + GpuDriverHelper.initializeDriverParameters() + NativeLibrary.restorePersistedGameDirectoryAccess() + } +} diff --git a/src/android/app/src/main/java/org/nxemu/SettingsIdentifiers.kt b/src/android/app/src/main/java/org/nxemu/SettingsIdentifiers.kt new file mode 100644 index 00000000..d4a7a9c9 --- /dev/null +++ b/src/android/app/src/main/java/org/nxemu/SettingsIdentifiers.kt @@ -0,0 +1,12 @@ +package org.nxemu + +object NXUISetting { + const val GameDirectories = "nxui:GameDirectories" +} + +object NXCoreSetting { + const val GpuHookLibDir = "nxcore:GpuHookLibDir" + const val GpuCustomDriverDir = "nxcore:GpuCustomDriverDir" + const val GpuCustomDriverName = "nxcore:GpuCustomDriverName" + const val GpuFileRedirectDir = "nxcore:GpuFileRedirectDir" +} diff --git a/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt index 5c4cfc5c..e1ddc8cf 100644 --- a/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/nxemu/ui/main/MainActivity.kt @@ -1,16 +1,78 @@ package org.nxemu.ui.main -import android.app.Activity import android.graphics.Color +import android.net.Uri import android.os.Bundle +import android.util.Log +import android.webkit.ConsoleMessage +import android.webkit.WebChromeClient import android.webkit.WebView +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import org.json.JSONArray +import org.nxemu.NXUISetting +import org.nxemu.NativeLibrary + +class MainActivity : ComponentActivity() { + private lateinit var webView: WebView + + private val addGameDirectory = registerForActivityResult( + ActivityResultContracts.OpenDocumentTree() + ) { uri: Uri? -> + uri?.let { + contentResolver.takePersistableUriPermission( + it, + android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + Log.d("NxEmu", "Folder selected: $it") + + val path = it.toString() + val existing = NativeLibrary.getSettingString(NXUISetting.GameDirectories) + val dirs = try { + JSONArray(existing) + } catch (e: Exception) { + JSONArray() + } + val paths = (0 until dirs.length()).map { i -> dirs.getString(i) } + if (!paths.contains(path)) { + dirs.put(path) + NativeLibrary.setSettingString(NXUISetting.GameDirectories, dirs.toString()) + NativeLibrary.saveSettings() + } + } + } -class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(WebView(this).apply { + webView = WebView(this).apply { setBackgroundColor(Color.BLACK) - loadUrl("file:///android_asset/index.html") - }) + settings.javaScriptEnabled = true + webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(msg: ConsoleMessage): Boolean { + Log.d("NxEmu-JS", "${msg.message()} [${msg.sourceId()}:${msg.lineNumber()}]") + return true + } + } + addJavascriptInterface(NxEmuBridge(this@MainActivity), "NxEmu") + } + NativeLibrary.onSettingChangedListener = { setting -> + runOnUiThread { + webView.evaluateJavascript( + "onSettingChanged('${setting.replace("'", "\\'")}')", + null + ) + } + } + webView.loadUrl("file:///android_asset/index.html") + setContentView(webView) + } + + override fun onDestroy() { + NativeLibrary.onSettingChangedListener = null + super.onDestroy() + } + + fun launchAddGameDirectory() { + addGameDirectory.launch(null) } } diff --git a/src/android/app/src/main/java/org/nxemu/ui/main/NxEmuBridge.kt b/src/android/app/src/main/java/org/nxemu/ui/main/NxEmuBridge.kt new file mode 100644 index 00000000..6cc06287 --- /dev/null +++ b/src/android/app/src/main/java/org/nxemu/ui/main/NxEmuBridge.kt @@ -0,0 +1,26 @@ +package org.nxemu.ui.main + +import android.webkit.JavascriptInterface +import org.nxemu.NativeLibrary + +class NxEmuBridge(private val activity: MainActivity) { + @JavascriptInterface + fun addGameDirectory() { + activity.runOnUiThread { activity.launchAddGameDirectory() } + } + + @JavascriptInterface + fun getSettingString(setting: String): String { + return NativeLibrary.getSettingString(setting) + } + + @JavascriptInterface + fun setSettingString(setting: String, value: String) { + NativeLibrary.setSettingString(setting, value) + } + + @JavascriptInterface + fun saveSettings() { + NativeLibrary.saveSettings() + } +} diff --git a/src/android/app/src/main/java/org/nxemu/utils/AndroidDevice.kt b/src/android/app/src/main/java/org/nxemu/utils/AndroidDevice.kt new file mode 100644 index 00000000..9147cc2b --- /dev/null +++ b/src/android/app/src/main/java/org/nxemu/utils/AndroidDevice.kt @@ -0,0 +1,21 @@ +package org.nxemu.utils + +import android.content.Context +import java.io.IOException +import org.nxemu.NxEmuApplication + +object AndroidDevice { + val PACKAGE_DIRECTORY: String + get() { + val filesDir = NxEmuApplication.appContext.filesDir + return try { + filesDir.canonicalPath + } catch (_: IOException) { + filesDir.absolutePath + } + } + + fun nativeModuleLibsDirectory(context: Context): String { + return context.applicationInfo.nativeLibraryDir + } +} diff --git a/src/android/app/src/main/java/org/nxemu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/nxemu/utils/GpuDriverHelper.kt new file mode 100644 index 00000000..94615fe2 --- /dev/null +++ b/src/android/app/src/main/java/org/nxemu/utils/GpuDriverHelper.kt @@ -0,0 +1,58 @@ +package org.nxemu.utils + +import java.io.File +import java.io.IOException +import org.json.JSONObject +import org.nxemu.NXCoreSetting +import org.nxemu.NativeLibrary +import org.nxemu.NxEmuApplication + +object GpuDriverHelper { + private const val META_JSON_FILENAME = "meta.json" + + fun initializeDriverParameters() { + val context = NxEmuApplication.appContext + val filesDir = context.filesDir + val publicDir = context.getExternalFilesDir(null) ?: filesDir + + val fileRedirectDir = File(publicDir, "gpu/vk_file_redirect") + val driverInstallDir = File(filesDir, "gpu_driver") + fileRedirectDir.mkdirs() + driverInstallDir.mkdirs() + + val hookLibDir = context.applicationInfo.nativeLibraryDir.trimEnd('/') + "/" + NativeLibrary.setSettingString(NXCoreSetting.GpuHookLibDir, hookLibDir) + NativeLibrary.setSettingString( + NXCoreSetting.GpuCustomDriverDir, + canonicalPath(driverInstallDir).trimEnd('/') + "/" + ) + NativeLibrary.setSettingString( + NXCoreSetting.GpuFileRedirectDir, + canonicalPath(fileRedirectDir).trimEnd('/') + "/" + ) + NativeLibrary.setSettingString( + NXCoreSetting.GpuCustomDriverName, + installedCustomDriverLibraryName(driverInstallDir) + ) + } + + private fun installedCustomDriverLibraryName(driverInstallDir: File): String { + val meta = File(driverInstallDir, META_JSON_FILENAME) + if (!meta.isFile) { + return "" + } + return try { + JSONObject(meta.readText()).optString("libraryName", "") + } catch (_: Exception) { + "" + } + } + + private fun canonicalPath(file: File): String { + return try { + file.canonicalPath + } catch (_: IOException) { + file.absolutePath + } + } +} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt new file mode 100644 index 00000000..04a267ed --- /dev/null +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,19 @@ +add_library(nxemu-android SHARED + native.cpp + notification.cpp + settings/settings_bridge.cpp + settings/ui_settings.cpp +) + +target_include_directories(nxemu-android PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(nxemu-android PRIVATE + nxemu_core + yuzu_common + common + android + log +) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp new file mode 100644 index 00000000..e383031f --- /dev/null +++ b/src/android/app/src/main/jni/native.cpp @@ -0,0 +1,92 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include "notification.h" +#include "settings/settings_bridge.h" + +namespace +{ + constexpr const char * kLogTag = "NxEmu"; + + jclass g_native_library_class = nullptr; + + std::string JStringToUtf8(JNIEnv * env, jstring value) + { + if (value == nullptr) + { + return {}; + } + const char * chars = env->GetStringUTFChars(value, nullptr); + std::string result = chars != nullptr ? chars : ""; + if (chars != nullptr) + { + env->ReleaseStringUTFChars(value, chars); + } + return result; + } +} + +extern "C" JNIEXPORT void JNICALL +Java_org_nxemu_NativeLibrary_appInit(JNIEnv * env, jclass /*clazz*/, jstring appDirectory, + jstring nativeModuleLibDir) +{ + JavaVM * vm = nullptr; + env->GetJavaVM(&vm); + SetJavaVM(vm); + + if (g_native_library_class != nullptr) + { + env->DeleteGlobalRef(g_native_library_class); + g_native_library_class = nullptr; + } + + jclass local_class = env->FindClass("org/nxemu/NativeLibrary"); + g_native_library_class = static_cast(env->NewGlobalRef(local_class)); + SetNativeLibraryGlobalRef(env->NewGlobalRef(local_class)); + env->DeleteLocalRef(local_class); + + const std::string baseDir = JStringToUtf8(env, appDirectory); + const std::string moduleDir = JStringToUtf8(env, nativeModuleLibDir); + Common::FS::SetAppDirectory(std::string(baseDir)); + + __android_log_print(ANDROID_LOG_INFO, kLogTag, "_____ _____ "); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "| | |_ _| __|_____ _ _ "); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "| | | |_'_| __| | | |"); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "|_|___|_,_|_____|_|_|_|___|"); + __android_log_print(ANDROID_LOG_INFO, kLogTag, ""); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "https://www.nxemu.com/"); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "%s Version %s", VER_FILE_DESCRIPTION_STR, VER_FILE_VERSION_STR); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "baseDir: %s", baseDir.c_str()); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "moduleDir: %s", moduleDir.c_str()); + + if (!AppInit(&AndroidNotification::GetInstance(), moduleDir.c_str(), baseDir.c_str())) + { + __android_log_print(ANDROID_LOG_ERROR, kLogTag, "AppInit failed (app=%s module=%s)", baseDir.c_str(), moduleDir.c_str()); + return; + } + + SettingsStore & store = SettingsStore::GetInstance(); + store.SetString(NXCoreSetting::ModuleDirectory, moduleDir.c_str()); + SettingsChange_Start(vm, env); +} + +extern "C" JNIEXPORT void JNICALL +Java_org_nxemu_NativeLibrary_appCleanup(JNIEnv * env, jclass /*clazz*/) +{ + SettingsChange_Stop(env); + AppCleanup(); + ClearNativeLibraryGlobalRef(env); + if (g_native_library_class != nullptr) + { + env->DeleteGlobalRef(g_native_library_class); + g_native_library_class = nullptr; + } +} diff --git a/src/android/app/src/main/jni/notification.cpp b/src/android/app/src/main/jni/notification.cpp new file mode 100644 index 00000000..5e6685c6 --- /dev/null +++ b/src/android/app/src/main/jni/notification.cpp @@ -0,0 +1,43 @@ +#include "notification.h" +#include "settings/ui_settings.h" +#include + +namespace +{ + constexpr const char * kLogTag = "NxEmu"; +} + +AndroidNotification::AndroidNotification() +{ +} + +void AndroidNotification::DisplayError(const char * message, const char * title) const +{ + __android_log_print(ANDROID_LOG_ERROR, kLogTag, "%s: %s", title != nullptr ? title : "", + message != nullptr ? message : ""); +} + +NotificationResponse AndroidNotification::Query(const char * message, const char * title) const +{ + __android_log_print(ANDROID_LOG_WARN, kLogTag, "Query %s: %s", title != nullptr ? title : "", + message != nullptr ? message : ""); + return NotificationResponse::No; +} + +void AndroidNotification::BreakPoint(const char * fileName, uint32_t lineNumber) +{ + __android_log_print(ANDROID_LOG_ERROR, kLogTag, "BreakPoint %s:%u", + fileName != nullptr ? fileName : "", lineNumber); +} + +void AndroidNotification::AppInitDone() +{ + __android_log_print(ANDROID_LOG_INFO, kLogTag, "AppInit done"); + SetupUISetting(); +} + +AndroidNotification & AndroidNotification::GetInstance() +{ + static AndroidNotification instance; + return instance; +} diff --git a/src/android/app/src/main/jni/notification.h b/src/android/app/src/main/jni/notification.h new file mode 100644 index 00000000..50556e36 --- /dev/null +++ b/src/android/app/src/main/jni/notification.h @@ -0,0 +1,21 @@ +#pragma once +#include + +class AndroidNotification : + public INotification +{ +public: + AndroidNotification(); + + // INotification + void DisplayError(const char * message, const char * title) const override; + NotificationResponse Query(const char * message, const char * title) const override; + void BreakPoint(const char * fileName, uint32_t lineNumber) override; + void AppInitDone() override; + + static AndroidNotification & GetInstance(); + +private: + AndroidNotification(const AndroidNotification &) = delete; + AndroidNotification & operator=(const AndroidNotification &) = delete; +}; diff --git a/src/android/app/src/main/jni/settings/settings_bridge.cpp b/src/android/app/src/main/jni/settings/settings_bridge.cpp new file mode 100644 index 00000000..c15c9d45 --- /dev/null +++ b/src/android/app/src/main/jni/settings/settings_bridge.cpp @@ -0,0 +1,92 @@ +#include "settings_bridge.h" +#include "ui_identifiers.h" +#include "ui_settings.h" +#include +#include +#include + +namespace +{ + constexpr const char * kLogTag = "NxEmu"; + + jclass s_native_library_class = nullptr; + jmethodID s_on_setting_changed = nullptr; + + void OnSettingChanged(const char * setting, void * /*userData*/) + { + __android_log_print(ANDROID_LOG_INFO, kLogTag, "OnSettingChanged setting: %s", setting); + if (s_native_library_class == nullptr || s_on_setting_changed == nullptr || setting == nullptr) + { + return; + } + + JNIEnv * env = GetEnvForThread(); + if (env == nullptr) + { + return; + } + + jstring key = env->NewStringUTF(setting); + env->CallStaticVoidMethod(s_native_library_class, s_on_setting_changed, key); + env->DeleteLocalRef(key); + if (env->ExceptionCheck()) + { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + } +} + +extern "C" JNIEXPORT jstring JNICALL +Java_org_nxemu_NativeLibrary_getSettingString(JNIEnv * env, jclass /*clazz*/, jstring setting) +{ + const char * key = env->GetStringUTFChars(setting, nullptr); + const char * value = SettingsStore::GetInstance().GetString(key); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "Get Setting key: %s", key); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "Get Setting value: %s", value); + env->ReleaseStringUTFChars(setting, key); + return env->NewStringUTF(value != nullptr ? value : ""); +} + +extern "C" JNIEXPORT void JNICALL +Java_org_nxemu_NativeLibrary_setSettingString(JNIEnv * env, jclass /*clazz*/, jstring setting, + jstring value) +{ + const char * key = env->GetStringUTFChars(setting, nullptr); + const char * val = env->GetStringUTFChars(value, nullptr); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "Set Setting key: %s", key); + __android_log_print(ANDROID_LOG_INFO, kLogTag, "Set Setting value: %s", val); + SettingsStore::GetInstance().SetString(key, val); + env->ReleaseStringUTFChars(value, val); + env->ReleaseStringUTFChars(setting, key); +} + +extern "C" JNIEXPORT void JNICALL +Java_org_nxemu_NativeLibrary_saveSettings(JNIEnv * /*env*/, jclass /*clazz*/) +{ + SaveUISetting(); +} + +void SettingsChange_Start(JavaVM * /*javaVm*/, JNIEnv * env) +{ + const jclass cls = env->FindClass("org/nxemu/NativeLibrary"); + s_native_library_class = reinterpret_cast(env->NewGlobalRef(cls)); + s_on_setting_changed = env->GetStaticMethodID(cls, "onSettingChanged", "(Ljava/lang/String;)V"); + env->DeleteLocalRef(cls); + + SettingsStore & store = SettingsStore::GetInstance(); + store.RegisterCallback(NXUISetting::GameDirectories, OnSettingChanged, nullptr); +} + +void SettingsChange_Stop(JNIEnv * env) +{ + SettingsStore & store = SettingsStore::GetInstance(); + store.UnregisterCallback(NXUISetting::GameDirectories, OnSettingChanged, nullptr); + + s_on_setting_changed = nullptr; + if (s_native_library_class != nullptr) + { + env->DeleteGlobalRef(s_native_library_class); + s_native_library_class = nullptr; + } +} diff --git a/src/android/app/src/main/jni/settings/settings_bridge.h b/src/android/app/src/main/jni/settings/settings_bridge.h new file mode 100644 index 00000000..fe57c96f --- /dev/null +++ b/src/android/app/src/main/jni/settings/settings_bridge.h @@ -0,0 +1,5 @@ +#pragma once +#include + +void SettingsChange_Start(JavaVM * javaVm, JNIEnv * env); +void SettingsChange_Stop(JNIEnv * env); diff --git a/src/android/app/src/main/jni/settings/ui_identifiers.h b/src/android/app/src/main/jni/settings/ui_identifiers.h new file mode 100644 index 00000000..7c63f172 --- /dev/null +++ b/src/android/app/src/main/jni/settings/ui_identifiers.h @@ -0,0 +1,7 @@ +#pragma once + +namespace NXUISetting +{ +constexpr const char * GameDirectories = "nxui:GameDirectories"; + +} // namespace NXUISetting \ No newline at end of file diff --git a/src/android/app/src/main/jni/settings/ui_settings.cpp b/src/android/app/src/main/jni/settings/ui_settings.cpp new file mode 100644 index 00000000..3ab9dd82 --- /dev/null +++ b/src/android/app/src/main/jni/settings/ui_settings.cpp @@ -0,0 +1,192 @@ +#include "ui_identifiers.h" +#include "ui_settings.h" +#include +#include +#include +#include +#include + +namespace +{ + enum class SettingType + { + StringList, + }; + + std::string SerializeStringList(const Stringlist & list) + { + JsonValue jsonArray(JsonValueType::Array); + for (const std::string & item : list) + { + jsonArray.Append(JsonValue(item)); + } + return JsonStyledWriter().write(jsonArray); + } + + class UISetting + { + public: + UISetting(const char * id, const char * key, Stringlist * value); + + const char * identifier; + const char * json_key; + SettingType settingType; + union + { + Stringlist * string_list; + } setting; + }; + + static UISetting settings[] = { + {NXUISetting::GameDirectories, "GameDirectories", &uiSettings.gameDirectories}, + }; + + void UISettingChanged(const char * setting, void * /*userData*/); + +} // namespace + +UISettings uiSettings = {}; + +void SetupUISetting() +{ + __android_log_print(ANDROID_LOG_INFO, "NxEmu", "SetupUISetting"); + SettingsStore & settingsStore = SettingsStore::GetInstance(); + + uiSettings = {}; + for (const UISetting & setting : settings) + { + switch (setting.settingType) + { + case SettingType::StringList: + *(setting.setting.string_list) = {}; + break; + default: + g_notify->BreakPoint(__FILE__, __LINE__); + } + } + + JsonValue section = SettingsStore::GetInstance().GetSettings("UI"); + for (const UISetting & setting : settings) + { + JsonValue value = JsonGetNestedValue(section, setting.json_key); + switch (setting.settingType) + { + case SettingType::StringList: + if (value.isArray()) + { + for (uint32_t i = 0; i < value.size(); i++) + { + if (!value[i].isString()) + { + continue; + } + setting.setting.string_list->push_back(value[i].asString()); + } + } + break; + default: + g_notify->BreakPoint(__FILE__, __LINE__); + } + + + if (setting.identifier != nullptr) + { + switch (setting.settingType) + { + case SettingType::StringList: + settingsStore.SetDefaultString(setting.identifier, ""); + settingsStore.SetString(setting.identifier, setting.setting.string_list != nullptr ? SerializeStringList(*setting.setting.string_list).c_str() : ""); + break; + default: + g_notify->BreakPoint(__FILE__, __LINE__); + } + + settingsStore.RegisterCallback(setting.identifier, UISettingChanged, nullptr); + } + } +} + +void SaveUISetting() +{ + JsonValue json; + for (const UISetting & setting : settings) + { + switch (setting.settingType) + { + case SettingType::StringList: + __android_log_print(ANDROID_LOG_INFO, "NxEmu", "SaveUISetting::stringList"); + if (!setting.setting.string_list->empty()) + { + JsonValue jsonList(JsonValueType::Array); + for (const std::string & item : *(setting.setting.string_list)) + { + __android_log_print(ANDROID_LOG_INFO, "NxEmu", "item: %s", item.c_str()); + jsonList.Append(JsonValue(item)); + } + JsonSetNestedValue(json, setting.json_key, std::move(jsonList)); + } + break; + default: + g_notify->BreakPoint(__FILE__, __LINE__); + } + } + + SettingsStore & settingstore = SettingsStore::GetInstance(); + settingstore.SetSettings("UI", json); + settingstore.Save(); +} + +namespace +{ + UISetting::UISetting(const char * id, const char * key, Stringlist * value) : + identifier(id), + json_key(key), + settingType(SettingType::StringList) + { + setting.string_list = value; + } + + void UISettingChanged(const char * setting, void * /*userData*/) + { + __android_log_print(ANDROID_LOG_INFO, "NxEmu", "UISettingChanged: setting = %s", setting); + SettingsStore & settingsStore = SettingsStore::GetInstance(); + + for (const UISetting & uiSetting : settings) + { + if (uiSetting.identifier == nullptr || strcmp(uiSetting.identifier, setting) != 0) + { + continue; + } + switch (uiSetting.settingType) + { + case SettingType::StringList: + if (uiSetting.setting.string_list != nullptr) + { + uiSetting.setting.string_list->clear(); + std::string json = settingsStore.GetString(setting); + __android_log_print(ANDROID_LOG_INFO, "NxEmu", "UISettingChanged: StringList json = %s", json.c_str()); + JsonValue root; + if (!json.empty()) + { + JsonReader reader; + if (!reader.Parse(json.data(), json.data() + json.size(), root)) + { + return; + } + if (root.isArray()) + { + for (uint32_t i = 0, n = root.size(); i < n; i++) + { + uiSetting.setting.string_list->push_back(root[i].asString()); + } + } + } + } + break; + default: + g_notify->BreakPoint(__FILE__, __LINE__); + } + break; + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/jni/settings/ui_settings.h b/src/android/app/src/main/jni/settings/ui_settings.h new file mode 100644 index 00000000..edef28b4 --- /dev/null +++ b/src/android/app/src/main/jni/settings/ui_settings.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +typedef std::vector Stringlist; + +struct UISettings +{ + Stringlist gameDirectories; +}; + +extern UISettings uiSettings; + +void SetupUISetting(); +void SaveUISetting(); \ No newline at end of file