[frontend] Built-in auto updater (#3845)
Some checks are pending
tx-src / sources (push) Waiting to run
Check Strings / check-strings (push) Waiting to run

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
This commit is contained in:
crueter 2026-04-28 20:42:23 +02:00
parent 77decca678
commit 676b1aabfc
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
23 changed files with 856 additions and 375 deletions

View file

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

View file

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

View file

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

View file

@ -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());
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, "<init>", "()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(

View file

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