aboutsummaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorcrueter <crueter@eden-emu.dev>2026-04-28 20:42:23 +0200
committercrueter <crueter@eden-emu.dev>2026-04-28 20:42:23 +0200
commit676b1aabfcd5f755ba82da963763410d7842ddc6 (patch)
treef3a3780caa339dbe6bb3cdc771edb74f60f0dc7e /src/android
parent77decca678b99b388ddbde3517cb9bd5f065cb80 (diff)
[frontend] Built-in auto updater (#3845)
Checks latest release and opens a dialog containing the changelog, and allow the user to select a specific build to download. After downloading, it prompts the user to open it. On Windows, this just opens up the zip in File Explorer. In the future setup files will be available. On macOS this opens up the DMG in Finder so the user can drag it to the Applications folder. Android retains the auto-update functionality from before, but updated to the new scheme. Body/View on Forgejo are not implemented, that should be in a future PR. Additionally, moved some common httplib incantations to `Common::Net`. This will serve as the common network accessor and JSON parser from here on out. TODO: - [x] android :( - [x] Search for builds based on keywords, with weights towards certain builds (e.g. macOS will search for dmg then tar.gz, windows msvc then mingw/exe then zip, etc.) - [x] remove linux leftovers - [x] don't allow asset selection on platforms w/o assets - [x] nightly changelog should be in the real FUTURE IMPLEMENTATION: - [ ] Body/View on Forgejo for Android - [ ] Setup files for Windows (Eden/nightly are separate) -- maybe portable/setup selector? - [ ] Something else I'm forgetting Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3845
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/build.gradle.kts7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt24
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt36
-rw-r--r--src/android/app/src/main/jni/native.cpp108
-rw-r--r--src/android/app/src/main/res/values/strings.xml1
5 files changed, 96 insertions, 80 deletions
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<String> = mutableListOf()
+ ) {
+ fun addAsset(asset: String) {
+ assets.add(asset)
+ }
+ }
+
@JvmField
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
@@ -240,17 +252,7 @@ object NativeLibrary {
/**
* Checks for available updates.
*/
- external fun checkForUpdate(): Array<String>?
-
- /**
- * 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..1da275ab87 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,23 @@ 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")
withContext(Dispatchers.Main) {
showDownloadProgressDialog()
}
- val downloader = APKDownloader(apkUrl, apkFile)
+ val downloader = APKDownloader(asset, apkFile)
downloader.download(
onProgress = { progress ->
runOnUiThread {
@@ -248,7 +254,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 +283,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<UpdateChecker::Update> release = UpdateChecker::GetUpdate();
+ std::optional<Common::Net::Release> 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());
+ jclass updateResultClass = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary$UpdateResult");
+ if (!updateResultClass) {
+ LOG_ERROR(Frontend, "Could not find UpdateResult class");
+ return nullptr;
+ }
- env->SetObjectArrayElement(result, 0, jtag);
- env->SetObjectArrayElement(result, 1, jname);
- env->DeleteLocalRef(jtag);
- env->DeleteLocalRef(jname);
+ jmethodID updateResultCtor = env->GetMethodID(updateResultClass, "<init>", "()V");
- return result;
-}
+ if (!updateResultCtor) {
+ LOG_ERROR(Frontend, "Could not find UpdateResult ctor");
+ env->DeleteLocalRef(updateResultClass);
+ return nullptr;
+ }
-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());
-}
+ 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");
-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
+ 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);
}
- const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant);
+ env->DeleteLocalRef(jtag);
+ env->DeleteLocalRef(jtitle);
+ env->DeleteLocalRef(jbody);
+ env->DeleteLocalRef(jurl);
- const std::string url = fmt::format("https://{}/{}/{}",
- std::string{Common::g_build_auto_update_api},
- version_str, apk_filename);
+ env->DeleteLocalRef(updateResultClass);
- env->ReleaseStringUTFChars(tag, version_str);
- env->ReleaseStringUTFChars(artifact, artifact_str);
- env->ReleaseStringUTFChars(packageId, package_id_str);
- return env->NewStringUTF(url.c_str());
+ 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.
<string name="external_content">External Content</string>
<string name="add_folders">Add Folder</string>
+ <string name="percent">%1$d%%</string>
</resources>