From 57b7b08fd8a24370a96cb5869f4a03b6b2a8c91f Mon Sep 17 00:00:00 2001 From: crueter Date: Tue, 28 Apr 2026 13:33:10 -0400 Subject: [PATCH] Android impl Signed-off-by: crueter --- CMakeLists.txt | 5 - src/CMakeLists.txt | 10 ++ src/android/app/build.gradle.kts | 7 ++ .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 24 ++-- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 38 +++--- src/android/app/src/main/jni/native.cpp | 116 +++++++++--------- .../app/src/main/res/values/strings.xml | 1 + src/common/net/net.cpp | 5 +- 8 files changed, 115 insertions(+), 91 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b4e261fec9..1142585b6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -268,11 +268,6 @@ if (NOT EXISTS ${PROJECT_BINARY_DIR}/${compat_json}) file(WRITE ${PROJECT_BINARY_DIR}/${compat_json} "") endif() -if (YUZU_LEGACY) - message(WARNING "Making legacy build. Performance may suffer.") - add_compile_definitions(YUZU_LEGACY) -endif() - if (ARCHITECTURE_arm64 AND (ANDROID OR PLATFORM_LINUX)) set(HAS_NCE 1) add_compile_definitions(HAS_NCE=1) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7df229d9f4..ec064a0e34 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,16 @@ if (NIGHTLY_BUILD) add_compile_definitions(NIGHTLY_BUILD) endif() +if (YUZU_LEGACY) + message(WARNING "Making legacy build. Performance may suffer.") + add_compile_definitions(YUZU_LEGACY) +endif() + +if (GENSHIN_SPOOF) + message(WARNING "Making Genshin spoof build") + add_compile_definitions(GENSHIN_SPOOF) +endif() + # Set compilation flags if (MSVC AND NOT CXX_CLANG) set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE) diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 3279a2202f..69b9f850d0 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -88,6 +88,7 @@ android { "-DBUILD_TESTING=OFF", "-DYUZU_TESTS=OFF", "-DDYNARMIC_TESTS=OFF", + "-DENABLE_UPDATE_CHECKER=ON", *extraCMakeArgs.toTypedArray() ) ) @@ -192,6 +193,12 @@ android { manifestPlaceholders += mapOf("appNameBase" to "Eden") resValue("string", "app_name_suffixed", "Eden") + externalNativeBuild { + cmake { + arguments.add("-DGENSHIN_SPOOF=ON") + } + } + ndk { abiFilters += listOf("arm64-v8a") } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 397b44c9f9..b21b43a53c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -33,6 +33,18 @@ import org.yuzu.yuzu_emu.applets.web.WebBrowser * with the native side of the Yuzu code. */ object NativeLibrary { + data class UpdateResult( + var tag: String = "", + var title: String = "", + var body: String = "", + var url: String = "", + var assets: MutableList = mutableListOf() + ) { + fun addAsset(asset: String) { + assets.add(asset) + } + } + @JvmField var sEmulationActivity = WeakReference(null) @@ -240,17 +252,7 @@ object NativeLibrary { /** * Checks for available updates. */ - external fun checkForUpdate(): Array? - - /** - * Return the URL to the release page - */ - external fun getUpdateUrl(version: String): String - - /** - * Return the URL to download the APK for the given version - */ - external fun getUpdateApkUrl(tag: String, artifact: String, packageId: String): String + external fun checkForUpdate(): UpdateResult? /** * Returns whether the update checker is enabled through CMAKE options. diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 584322df50..a6c1e1c9e6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -175,25 +175,25 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val latestVersion = NativeLibrary.checkForUpdate() if (latestVersion != null) { runOnUiThread { - val tag: String = latestVersion[0] - val name: String = latestVersion[1] - showUpdateDialog(tag, name) + showUpdateDialog(latestVersion) } } }.start() } - private fun showUpdateDialog(tag: String, name: String) { + // TODO(crueter): body, "View on Forgejo" button + private fun showUpdateDialog(release: NativeLibrary.UpdateResult) { MaterialAlertDialogBuilder(this) .setTitle(R.string.update_available) - .setMessage(getString(R.string.update_available_description, name)) + .setMessage(getString(R.string.update_available_description, release.title)) .setPositiveButton(android.R.string.ok) { _, _ -> - var artifact = tag - // Nightly builds have a slightly different format - if (NativeLibrary.isNightlyBuild()) { - artifact = tag.substringAfter('.', tag) + val assets = release.assets + + if (assets.isEmpty()) { + openLink(release.url) + } else { + downloadAndInstallUpdate(release) } - downloadAndInstallUpdate(tag, artifact) } .setNeutralButton(R.string.cancel) { dialog, _ -> dialog.dismiss() @@ -206,17 +206,25 @@ class MainActivity : AppCompatActivity(), ThemeProvider { .show() } - private fun downloadAndInstallUpdate(version: String, artifact: String) { + private fun openLink(link: String) { + val intent = Intent(Intent.ACTION_VIEW, link.toUri()) + startActivity(intent) + } + + private fun downloadAndInstallUpdate(release: NativeLibrary.UpdateResult) { CoroutineScope(Dispatchers.IO).launch { val packageId = applicationContext.packageName - val apkUrl = NativeLibrary.getUpdateApkUrl(version, artifact, packageId) + val asset = release.assets[0] + val artifact = asset.split("/").last() val apkFile = File(cacheDir, "update-$artifact.apk") + Log.info("Artifact: ${artifact}\nAsset: ${asset}") + withContext(Dispatchers.Main) { showDownloadProgressDialog() } - val downloader = APKDownloader(apkUrl, apkFile) + val downloader = APKDownloader(asset, apkFile) downloader.download( onProgress = { progress -> runOnUiThread { @@ -248,7 +256,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } else { Toast.makeText( this@MainActivity, - getString(R.string.update_download_failed) + "\n\nURL: $apkUrl", + getString(R.string.update_download_failed) + "\n\nURL: $asset", Toast.LENGTH_LONG ).show() } @@ -277,7 +285,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private fun updateDownloadProgress(progress: Int) { progressBar?.progress = progress - progressMessage?.text = "$progress%" + progressMessage?.text = getString(R.string.percent, progress) } private fun dismissDownloadProgressDialog() { diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index b78be0cefa..abfc428919 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -1699,76 +1699,76 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isNightlyBuild( #ifdef ENABLE_UPDATE_CHECKER -JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate( +JNIEXPORT jobject JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate( JNIEnv* env, jobject obj) { - std::optional release = UpdateChecker::GetUpdate(); + std::optional release = UpdateChecker::GetUpdate(); if (!release) return nullptr; const std::string tag = release->tag; - const std::string name = release->name; + const std::string title = release->title; + const std::string body = release->body; + const std::string url = release->html_url; - jobjectArray result = env->NewObjectArray(2, env->FindClass("java/lang/String"), nullptr); + // Android *should* only ever define a single asset. + // If not, something has gone wrong, but the Kotlin side can handle it. + const auto assets = release->GetPlatformAssets(); - const jstring jtag = env->NewStringUTF(tag.c_str()); - const jstring jname = env->NewStringUTF(name.c_str()); - - env->SetObjectArrayElement(result, 0, jtag); - env->SetObjectArrayElement(result, 1, jname); - env->DeleteLocalRef(jtag); - env->DeleteLocalRef(jname); - - return result; -} - -JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateUrl( - JNIEnv* env, - jobject obj, - jstring version) { - const char* version_str = env->GetStringUTFChars(version, nullptr); - const std::string url = fmt::format("{}/{}", - std::string{Common::g_build_auto_update_api}, - version_str); - env->ReleaseStringUTFChars(version, version_str); - return env->NewStringUTF(url.c_str()); -} - -JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl( - JNIEnv* env, - jobject obj, - jstring tag, - jstring artifact, - jstring packageId) { - const char* version_str = env->GetStringUTFChars(tag, nullptr); - const char* artifact_str = env->GetStringUTFChars(artifact, nullptr); - const char* package_id_str = env->GetStringUTFChars(packageId, nullptr); - - std::string variant; - std::string package_id(package_id_str); - - if (package_id.find("dev.legacy.eden_emulator") != std::string::npos) { - variant = "legacy"; - } else if (package_id.find("com.miHoYo.Yuanshen") != std::string::npos) { - variant = "optimized"; - } else { -#ifdef ARCHITECTURE_arm64 - variant = "standard"; -#else - variant = "chromeos"; -#endif + jclass updateResultClass = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary$UpdateResult"); + if (!updateResultClass) { + LOG_ERROR(Frontend, "Could not find UpdateResult class"); + return nullptr; } - const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant); + jmethodID updateResultCtor = env->GetMethodID(updateResultClass, "", "()V"); - const std::string url = fmt::format("https://{}/{}/{}", - std::string{Common::g_build_auto_update_api}, - version_str, apk_filename); + if (!updateResultCtor) { + LOG_ERROR(Frontend, "Could not find UpdateResult ctor"); + env->DeleteLocalRef(updateResultClass); + return nullptr; + } - env->ReleaseStringUTFChars(tag, version_str); - env->ReleaseStringUTFChars(artifact, artifact_str); - env->ReleaseStringUTFChars(packageId, package_id_str); - return env->NewStringUTF(url.c_str()); + jmethodID setTag = env->GetMethodID(updateResultClass, "setTag", "(Ljava/lang/String;)V"); + jmethodID setTitle = env->GetMethodID(updateResultClass, "setTitle", "(Ljava/lang/String;)V"); + jmethodID setBody = env->GetMethodID(updateResultClass, "setBody", "(Ljava/lang/String;)V"); + jmethodID setUrl = env->GetMethodID(updateResultClass, "setUrl", "(Ljava/lang/String;)V"); + jmethodID addAsset = env->GetMethodID(updateResultClass, "addAsset", "(Ljava/lang/String;)V"); + + jobject updateResult = env->NewObject(updateResultClass, updateResultCtor); + + LOG_DEBUG(Frontend, "Tag: {}", tag); + LOG_DEBUG(Frontend, "Title: {}", title); + LOG_DEBUG(Frontend, "Body: {}", body); + LOG_DEBUG(Frontend, "Url: {}", url); + + const auto jtag = env->NewStringUTF(tag.c_str()); + const auto jtitle = env->NewStringUTF(title.c_str()); + const auto jbody = env->NewStringUTF(body.c_str()); + const auto jurl = env->NewStringUTF(url.c_str()); + + env->CallVoidMethod(updateResult, setTag, jtag); + env->CallVoidMethod(updateResult, setTitle, jtitle); + env->CallVoidMethod(updateResult, setBody, jbody); + env->CallVoidMethod(updateResult, setUrl, jurl); + + // TODO(crueter): Handling for multiple assets? + // Maybe another data class x( + for (const Common::Net::Asset &a : assets) { + const auto jaurl = env->NewStringUTF(a.path.c_str()); + env->CallVoidMethod(updateResult, addAsset, jaurl); + env->DeleteLocalRef(jaurl); + } + + env->DeleteLocalRef(jtag); + env->DeleteLocalRef(jtitle); + env->DeleteLocalRef(jbody); + env->DeleteLocalRef(jurl); + + env->DeleteLocalRef(updateResultClass); + + return updateResult; } + #endif JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion( diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 7052a7718a..d143a12f4e 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1783,5 +1783,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. External Content Add Folder + %1$d%% diff --git a/src/common/net/net.cpp b/src/common/net/net.cpp index ef1fcf0c3e..84c62176f2 100644 --- a/src/common/net/net.cpp +++ b/src/common/net/net.cpp @@ -78,9 +78,10 @@ std::vector Release::GetPlatformAssets() const { #elif defined(ARCHITECTURE_arm64) #ifdef YUZU_LEGACY find_asset("Standard", {"legacy.apk"}); +#elif defined(GENSHIN_SPOOF) + find_asset("Standard", {"optimized.apk"}); #else find_asset("Standard", {"standard.apk"}); - find_asset("Genshin Spoof", {"optimized.apk"}); #endif #endif #endif @@ -132,7 +133,7 @@ std::optional Release::FromJson(const nlohmann::json& json, const std:: rel.host = host; const auto release_base = - fmt::format("{}/{}/releases", Common::g_build_auto_update_stable_api, repo); + fmt::format("{}/{}/releases", host, repo); const auto fallback_html = fmt::format("{}/tag/{}", release_base, rel.tag); rel.html_url = json.value("html_url", fallback_html);