From d34a87c1f10c662d51fcccae01dc17b9345acd3e Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Fri, 18 Sep 2026 13:26:13 +0200 Subject: [PATCH] Make the Android updater read GitHub atom feeds the same way desktop does. Co-authored-by: Cursor --- android/app/build.gradle.kts | 4 +- .../org/maturita/maturita/update/Updater.kt | 75 +++++++++++++------ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9eddf4c..0abb597 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "org.maturita.maturita" minSdk = 26 targetSdk = 35 - versionCode = 4 - versionName = "android-4" + versionCode = 5 + versionName = "android-5" buildConfigField("String", "COMMIT", "\"$commit\"") buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"") buildConfigField("String", "UPDATE_BRANCH", "\"builds\"") diff --git a/android/app/src/main/java/org/maturita/maturita/update/Updater.kt b/android/app/src/main/java/org/maturita/maturita/update/Updater.kt index dad9196..74292ef 100644 --- a/android/app/src/main/java/org/maturita/maturita/update/Updater.kt +++ b/android/app/src/main/java/org/maturita/maturita/update/Updater.kt @@ -2,8 +2,11 @@ package org.maturita.maturita.update import android.app.Application import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Handler import android.os.Looper +import android.provider.Settings import androidx.core.content.FileProvider import org.maturita.maturita.BuildConfig import java.io.File @@ -22,6 +25,8 @@ data class UpdateState( class Updater(private val app: Application) { private val io = Executors.newSingleThreadExecutor() private val main = Handler(Looper.getMainLooper()) + private val repo = BuildConfig.UPDATE_REPO + private val branch = BuildConfig.UPDATE_BRANCH private fun emit(cb: (UpdateState) -> Unit, state: UpdateState) { main.post { cb(state) } @@ -35,18 +40,17 @@ class Updater(private val app: Application) { emit(cb, UpdateState(status = "checking", messageKey = "update_checking", messageArg = null)) io.execute { try { - val atom = get("https://github.com/${BuildConfig.UPDATE_REPO}/commits/${BuildConfig.UPDATE_BRANCH}.atom") - val sha = Regex(""".*?/commit/([0-9a-f]{7,40})""", RegexOption.IGNORE_CASE) - .find(atom)?.groupValues?.get(1) - ?: throw IllegalStateException("sha") - val version = get( - "https://raw.githubusercontent.com/${BuildConfig.UPDATE_REPO}/$sha/VERSION", - ).trim() + val atom = get("https://github.com/$repo/commits/$branch.atom") + val remote = atomMainSha(atom) + ?: run { + val tip = atomTip(atom) ?: error("sha") + get("https://raw.githubusercontent.com/$repo/$tip/VERSION").trim() + } val local = BuildConfig.COMMIT - if (version.startsWith(local) || local.startsWith(version)) { - emit(cb, UpdateState("ok", version, "update_uptodate", version.take(7))) + if (sameCommit(remote, local)) { + emit(cb, UpdateState("ok", remote, "update_uptodate", remote.take(7))) } else { - emit(cb, UpdateState("available", version, "update_available", version.take(7), true)) + emit(cb, UpdateState("available", remote, "update_available", remote.take(7), true)) } } catch (_: Exception) { emit(cb, UpdateState("error", messageKey = "update_err_network", messageArg = null)) @@ -58,11 +62,20 @@ class Updater(private val app: Application) { emit(cb, UpdateState(status = "downloading", messageKey = "update_downloading", messageArg = null)) io.execute { try { - val atom = get("https://github.com/${BuildConfig.UPDATE_REPO}/commits/${BuildConfig.UPDATE_BRANCH}.atom") - val sha = Regex(""".*?/commit/([0-9a-f]{7,40})""", RegexOption.IGNORE_CASE) - .find(atom)?.groupValues?.get(1) - ?: throw IllegalStateException("sha") - val url = "https://raw.githubusercontent.com/${BuildConfig.UPDATE_REPO}/$sha/maturita-android.apk" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + !app.packageManager.canRequestPackageInstalls() + ) { + val settings = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { + data = Uri.parse("package:${app.packageName}") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + main.post { app.startActivity(settings) } + emit(cb, UpdateState("error", messageKey = "update_err_download", messageArg = null)) + return@execute + } + val atom = get("https://github.com/$repo/commits/$branch.atom") + val tip = atomTip(atom) ?: error("sha") + val url = "https://raw.githubusercontent.com/$repo/$tip/maturita-android.apk" val dir = File(app.cacheDir, "updates").apply { mkdirs() } val apk = File(dir, "maturita.apk") download(url, apk) @@ -77,13 +90,25 @@ class Updater(private val app: Application) { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION } main.post { app.startActivity(intent) } - emit(cb, UpdateState("staged", sha, "update_staged", sha.take(7))) + emit(cb, UpdateState("staged", tip, "update_staged", tip.take(7))) } catch (_: Exception) { emit(cb, UpdateState("error", messageKey = "update_err_download", messageArg = null)) } } } + private fun atomTip(atom: String): String? = + Regex("""Commit/([0-9a-f]{40})""", RegexOption.IGNORE_CASE).find(atom)?.groupValues?.get(1) + ?: Regex("""/commit/([0-9a-f]{7,40})""", RegexOption.IGNORE_CASE).find(atom)?.groupValues?.get(1) + + private fun atomMainSha(atom: String): String? = + Regex("""Build from ([0-9a-f]{7,40})""", RegexOption.IGNORE_CASE).find(atom)?.groupValues?.get(1) + + private fun sameCommit(a: String, b: String): Boolean { + val n = minOf(a.length, b.length) + return n >= 7 && a.take(n).equals(b.take(n), ignoreCase = true) + } + private fun isApk(file: File): Boolean { if (file.length() < 1_000_000L) return false file.inputStream().use { input -> @@ -93,12 +118,18 @@ class Updater(private val app: Application) { } } - private fun get(url: String): String { - val c = (URL(url).openConnection() as HttpURLConnection).apply { + private fun open(url: String, readMs: Int): HttpURLConnection { + return (URL(url).openConnection() as HttpURLConnection).apply { connectTimeout = 15000 - readTimeout = 15000 + readTimeout = readMs instanceFollowRedirects = true + setRequestProperty("User-Agent", "maturita.c-android") + setRequestProperty("Accept", "*/*") } + } + + private fun get(url: String): String { + val c = open(url, 15000) return try { if (c.responseCode !in 200..299) error("http ${c.responseCode}") c.inputStream.bufferedReader().use { it.readText() } @@ -108,11 +139,7 @@ class Updater(private val app: Application) { } private fun download(url: String, dest: File) { - val c = (URL(url).openConnection() as HttpURLConnection).apply { - connectTimeout = 20000 - readTimeout = 60000 - instanceFollowRedirects = true - } + val c = open(url, 60000) try { if (c.responseCode !in 200..299) error("http ${c.responseCode}") c.inputStream.use { input -> dest.outputStream().use { input.copyTo(it) } }