diff --git a/src/android/.gitignore b/src/android/.gitignore index aa724b77..f4e580b2 100644 --- a/src/android/.gitignore +++ b/src/android/.gitignore @@ -13,3 +13,4 @@ .externalNativeBuild .cxx local.properties +app/src/main/assets/hash.txt diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 746ef6e8..da5927ab 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -1,4 +1,9 @@ +import com.android.build.gradle.internal.tasks.factory.dependsOn import java.io.IOException +import java.security.MessageDigest +import java.util.regex.Pattern +import javax.xml.bind.DatatypeConverter + plugins { alias(libs.plugins.android.application) @@ -35,6 +40,8 @@ fun getVersionName(): String { fun getVersionCode(): Int = System.getenv("VERSION_CODE")?.toIntOrNull() ?: 1 +val cemuDataFilesFolder = "../../../bin" + android { namespace = "info.cemu.cemu" compileSdk = 35 @@ -47,6 +54,17 @@ android { versionName = getVersionName() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + + androidResources { + ignoreAssetsPattern = "!*cemu.mo:" + } + + sourceSets.getByName("main") { + assets { + srcDir(cemuDataFilesFolder) + } + } + packaging { jniLibs.useLegacyPackaging = true } @@ -139,6 +157,58 @@ android { } } +abstract class ComputeCemuDataFilesHashTask : DefaultTask() { + private val ignoreFilePatterns = arrayOf( + Pattern.compile(".*cemu\\.mo"), + Pattern.compile(".*Cemu_(?:debug|release)"), + ) + + @get:Input + abstract val cemuDataFolder: Property + + private fun isFileIgnored(file: File): Boolean { + return ignoreFilePatterns.any { pattern -> pattern.matcher(file.path).matches() } + } + + @TaskAction + fun computeCemuDataFilesHash() { + val assetDir = File(project.projectDir, "src/main/assets") + if (!assetDir.exists()) { + assetDir.mkdirs() + } + + val cemuDataFilesFile = File(project.projectDir, cemuDataFolder.get()) + val hashFile = File(assetDir, "hash.txt") + val md = MessageDigest.getInstance("SHA-256") + + if (!cemuDataFilesFile.isDirectory) { + hashFile.writeText("invalid") + return + } + + val fileHashes = cemuDataFilesFile.walkTopDown() + .filter { it.isFile && !isFileIgnored(it) } + .sortedBy { it.path } + .map { + md.reset() + md.update(it.path.toByteArray()) + md.update(it.readBytes()) + md.digest() + } + .toList() + + md.reset() + fileHashes.forEach { md.update(it) } + + hashFile.writeText(DatatypeConverter.printHexBinary(md.digest())) + } +} + +tasks.register("computeCemuDataFilesHash") { + cemuDataFolder = cemuDataFilesFolder +} +tasks.preBuild.dependsOn("computeCemuDataFilesHash") + dependencies { implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.kotlinx.serialization.json) diff --git a/src/android/app/src/main/cpp/NativeActiveSettings.cpp b/src/android/app/src/main/cpp/NativeActiveSettings.cpp index 71993588..274249de 100644 --- a/src/android/app/src/main/cpp/NativeActiveSettings.cpp +++ b/src/android/app/src/main/cpp/NativeActiveSettings.cpp @@ -14,12 +14,13 @@ Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_getUserDataPath(JNIEnv* } extern "C" [[maybe_unused]] JNIEXPORT void JNICALL -Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_initializeActiveSettings(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring data_path, jstring cache_path) +Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_initializeActiveSettings(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring user_data_path, jstring data_path, jstring cache_path) { + std::string userDataPath = JNIUtils::toString(env, user_data_path); std::string dataPath = JNIUtils::toString(env, data_path); std::string cachePath = JNIUtils::toString(env, cache_path); std::set failedWriteAccess; - ActiveSettings::SetPaths(false, {}, dataPath, dataPath, cachePath, dataPath, failedWriteAccess); + ActiveSettings::SetPaths(false, {}, userDataPath, userDataPath, cachePath, dataPath, failedWriteAccess); } extern "C" [[maybe_unused]] JNIEXPORT void JNICALL diff --git a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt index 5594f7ef..d4afba7e 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt @@ -1,7 +1,6 @@ package info.cemu.cemu import android.app.Application -import android.util.Log import info.cemu.cemu.nativeinterface.NativeActiveSettings.initializeActiveSettings import info.cemu.cemu.nativeinterface.NativeActiveSettings.setInternalDir import info.cemu.cemu.nativeinterface.NativeActiveSettings.setNativeLibDir @@ -11,8 +10,10 @@ import info.cemu.cemu.nativeinterface.NativeGraphicPacks.refreshGraphicPacks import info.cemu.cemu.nativeinterface.NativeLogging.crashLog import info.cemu.cemu.nativeinterface.NativeSwkbd.initializeSwkbd import java.io.File +import java.io.IOException import java.io.PrintWriter import java.io.StringWriter +import java.util.regex.Pattern class CemuApplication : Application() { init { @@ -30,6 +31,78 @@ class CemuApplication : Application() { override fun onCreate() { super.onCreate() + configureExceptionHandler() + initializeCemu() + saveDataFiles() + } + + private fun saveDataFiles() { + val dataFolder = File(internalCemuDataFolder) + + if (!dataFolder.exists() && !dataFolder.mkdirs()) { + return + } + + val hashFileName = "hash.txt" + val hashFile = dataFolder.resolve(hashFileName) + val oldHash = if (hashFile.isFile) hashFile.readText() else "invalid" + + val assetsFileStream = try { + assets.open(hashFileName) + } catch (_: IOException) { + null + } + + if (assetsFileStream == null) { + return + } + + val newHash = assetsFileStream.reader().readText() + + if (oldHash == newHash) { + return + } + + dataFolder.deleteRecursively() + dataFolder.mkdirs() + dataFolder.resolve(hashFileName).writeText(newHash) + + fun traverseAssets(path: String = ""): Iterator = iterator { + val assetFiles = assets.list(path) ?: return@iterator + + if (assetFiles.isEmpty()) { + yield(path) + } + + for (assetFile in assetFiles) { + val assetPath = path + (if (path == "") "" else "/") + assetFile + for (file in traverseAssets(assetPath)) { + yield(file) + } + } + } + + val filePatterns = arrayOf( + Pattern.compile("gameProfiles/.*"), + Pattern.compile("resources/.*"), + ) + + fun isFileValid(file: String): Boolean { + return filePatterns.any { pattern -> pattern.matcher(file).matches() } + } + + for (assetFile in traverseAssets()) { + if (!isFileValid(assetFile)) { + continue + } + + val outFile = dataFolder.resolve(assetFile) + outFile.parentFile?.mkdirs() + assets.open(assetFile).copyTo(outFile.outputStream()) + } + } + + private fun configureExceptionHandler() { if (DefaultUncaughtExceptionHandler == null) { DefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() } @@ -44,9 +117,16 @@ class CemuApplication : Application() { exception ) } + } + + private fun initializeCemu() { val displayMetrics = resources.displayMetrics setDPI(displayMetrics.density) - initializeActiveSettings(internalFolder.toString(), internalFolder.toString()) + initializeActiveSettings( + userDataPath = internalCemuUserFolder, + dataPath = internalCemuDataFolder, + cachePath = internalCemuUserFolder, + ) setNativeLibDir(applicationInfo.nativeLibraryDir) setInternalDir(dataDir.absolutePath) initializeEmulation() @@ -54,6 +134,12 @@ class CemuApplication : Application() { refreshGraphicPacks() } + private val internalCemuDataFolder: String + get() = internalFolder.resolve("data").toString() + + private val internalCemuUserFolder: String + get() = internalFolder.toString() + companion object { init { System.loadLibrary("CemuAndroid") diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt index 7205494a..b92bc9e3 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeActiveSettings.kt @@ -2,7 +2,7 @@ package info.cemu.cemu.nativeinterface object NativeActiveSettings { @JvmStatic - external fun initializeActiveSettings(dataPath: String, cachePath: String) + external fun initializeActiveSettings(userDataPath: String, dataPath: String, cachePath: String) @JvmStatic external fun setNativeLibDir(nativeLibDir: String)