Android: get the application to initialize the c++ code and be able to set game directory (#198)
This commit is contained in:
parent
072cf46800
commit
d11dbac744
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -47,3 +47,7 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.activity:activity-ktx:1.9.3")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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 |
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
#include <jni.h>
|
||||
|
||||
void SettingsChange_Start(JavaVM * javaVm, JNIEnv * env);
|
||||
void SettingsChange_Stop(JNIEnv * env);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
namespace NXUISetting
|
||||
{
|
||||
constexpr const char * GameDirectories = "nxui:GameDirectories";
|
||||
|
||||
} // namespace NXUISetting
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue