Android: get the application to initialize the c++ code and be able to set game directory (#198)

This commit is contained in:
NullVector 2026-09-05 20:00:58 +09:30 committed by GitHub
parent 072cf46800
commit d11dbac744
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1072 additions and 19 deletions

View File

@ -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()

View File

@ -47,3 +47,7 @@ android {
}
}
}
dependencies {
implementation("androidx.activity:activity-ktx:1.9.3")
}

View File

@ -2,14 +2,20 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="@string/app_name"
android:name="org.nxemu.NxEmuApplication"
android:label="NxEmu"
android:isGame="true"
android:appCategory="game"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar">
<activity
android:name="org.nxemu.ui.main.MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.GAME" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>

View File

@ -1,20 +1,308 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
html, body {
height: 100%;
margin: 0;
background: #000;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font: 24px sans-serif;
:root {
--bg: #ffffff;
--text: #000000;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #121212;
--text: #e0e0e0;
}
}
body {
background-color: var(--bg);
color: var(--text);
margin: 0;
font-family: sans-serif;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 56px;
}
.topbar-back {
display: flex;
align-items: center;
padding: 0 8px;
height: 56px;
}
.logo {
font-size: 24px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.settings-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
}
.settings-btn svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 2;
}
.back-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
}
.back-btn svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.page-title {
font-size: 20px;
font-weight: 700;
}
.settings-list {
padding: 8px 16px;
}
.settings-item {
padding: 16px 0;
font-size: 16px;
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
cursor: pointer;
}
.fab {
position: fixed;
bottom: 24px;
right: 20px;
width: 56px;
height: 56px;
border-radius: 16px;
border: none;
background: #4fc3f7;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.fab svg {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
}
.folder-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 0;
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}
.setting-info {
flex: 1;
min-width: 0;
}
.setting-label {
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.folder-remove {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.folder-remove svg {
width: 22px;
height: 22px;
fill: currentColor;
}
</style>
<script>
let navStack = ['gamesPage'];
function onSettingChanged(setting) {
if (setting === 'nxui:GameDirectories') {
renderGameFolders();
refreshGameList();
}
}
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
async function refreshGameList() {
}
function navigate(page) {
navStack.push(page);
showPage(page);
}
function goBack() {
if (navStack.length > 1) {
navStack.pop();
showPage(navStack[navStack.length - 1]);
}
}
function formatGameFolder(uri) {
const decoded = decodeURIComponent(uri);
const prefix = 'content://com.android.externalstorage.documents/tree/';
if (!decoded.startsWith(prefix)) return decoded;
const path = decoded.substring(prefix.length);
const colonIndex = path.indexOf(':');
const volume = path.substring(0, colonIndex);
const folder = path.substring(colonIndex + 1);
if (volume === 'primary') {
return 'Internal Storage:' + folder;
}
return volume + ':' + folder;
}
function renderGameFolders() {
const list = document.getElementById('folderList');
const empty = document.getElementById('noFolders');
let dirs;
try {
dirs = JSON.parse(NxEmu.getSettingString('nxui:GameDirectories') || '[]');
} catch (e) {
dirs = [];
}
empty.style.display = dirs.length === 0 ? '' : 'none';
list.innerHTML = '';
dirs.forEach((uri, index) => {
const name = formatGameFolder(uri);
const item = document.createElement('div');
item.className = 'folder-item';
item.innerHTML = `
<div class="setting-info">
<div class="setting-label">${escapeHtml(name)}</div>
</div>
<button class="folder-remove" onclick="removeGameFolder('${uri.replace(/'/g, "\\'")}')">
<svg viewBox="0 0 1024 1024"><path d="M160 256H96a32 32 0 0 1 0-64h256V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64h-64v672a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32zm448-64v-64H416v64zM224 896h576V256H224zm192-128a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32m192 0a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32"/></svg>
</button>
`;
list.appendChild(item);
});
}
function removeGameFolder(uri) {
let dirs;
try {
dirs = JSON.parse(NxEmu.getSettingString('nxui:GameDirectories') || '[]');
} catch (e) {
dirs = [];
}
dirs = dirs.filter(function (d) { return d !== uri; });
NxEmu.setSettingString('nxui:GameDirectories', JSON.stringify(dirs));
NxEmu.saveSettings();
}
function showPage(page) {
document.querySelectorAll('.page').forEach(p => p.style.display = 'none');
document.getElementById(page).style.display = '';
if (page === 'gameFolders') { renderGameFolders(); }
if (page === 'gamesPage') { refreshGameList(); }
}
</script>
</head>
<body>Hello from NxEmu</body>
<body>
<div id="gamesPage" class="page">
<div class="topbar">
<div class="logo"><img src="file:///android_asset/nxlogo-32x32.png" width="20" height="20" alt="">NxEmu</div>
<button class="settings-btn" onclick="navigate('settingsPage')">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
</button>
</div>
<div id="gameList" class="game-list">game list</div>
</div>
<div id="settingsPage" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="page-title">Settings</div>
</div>
<div class="settings-list">
<div class="settings-item" onclick="navigate('gameFolders')">
<span>Game Folders</span>
</div>
</div>
</div>
<div id="gameFolders" class="page" style="display:none">
<div class="topbar-back">
<button class="back-btn" onclick="goBack()">
<svg viewBox="0 0 24 24"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
</button>
<div class="page-title">Game Folders</div>
</div>
<div id="folderList" class="settings-list"></div>
<div id="noFolders" class="empty-hint">
<p>No game folders added</p>
</div>
<button class="fab" onclick="NxEmu.addGameDirectory()">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,63 @@
package org.nxemu
import android.util.Log
import org.json.JSONArray
object NativeLibrary {
init {
System.loadLibrary("nxemu-android")
}
var onSettingChangedListener: ((String) -> 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()
}

View File

@ -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()
}
}

View File

@ -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"
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
)

View File

@ -0,0 +1,92 @@
#include <android/log.h>
#include <jni.h>
#include <string>
#include <nxemu-core/app_init.h>
#include <nxemu-core/settings/identifiers.h>
#include <nxemu-core/settings/settings.h>
#include <nxemu-core/version.h>
#include <yuzu_common/android/java_bridge.h>
#include <yuzu_common/fs/fs_android.h>
#include <yuzu_common/fs/path_util.h>
#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<jclass>(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;
}
}

View File

@ -0,0 +1,43 @@
#include "notification.h"
#include "settings/ui_settings.h"
#include <android/log.h>
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;
}

View File

@ -0,0 +1,21 @@
#pragma once
#include <nxemu-core/notification.h>
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;
};

View File

@ -0,0 +1,92 @@
#include "settings_bridge.h"
#include "ui_identifiers.h"
#include "ui_settings.h"
#include <android/log.h>
#include <nxemu-core/settings/settings.h>
#include <yuzu_common/android/java_bridge.h>
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<jclass>(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;
}
}

View File

@ -0,0 +1,5 @@
#pragma once
#include <jni.h>
void SettingsChange_Start(JavaVM * javaVm, JNIEnv * env);
void SettingsChange_Stop(JNIEnv * env);

View File

@ -0,0 +1,7 @@
#pragma once
namespace NXUISetting
{
constexpr const char * GameDirectories = "nxui:GameDirectories";
} // namespace NXUISetting

View File

@ -0,0 +1,192 @@
#include "ui_identifiers.h"
#include "ui_settings.h"
#include <common/json_util.h>
#include <cstring>
#include <nxemu-core/settings/settings.h>
#include <nxemu-core/notification.h>
#include <android/log.h>
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;
}
}
}

View File

@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
typedef std::vector<std::string> Stringlist;
struct UISettings
{
Stringlist gameDirectories;
};
extern UISettings uiSettings;
void SetupUISetting();
void SaveUISetting();