Make the Android updater read GitHub atom feeds the same way desktop does.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-18 13:26:13 +02:00
commit d34a87c1f1
2 changed files with 53 additions and 26 deletions

View file

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

View file

@ -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("""<id>.*?/commit/([0-9a-f]{7,40})</id>""", 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("""<id>.*?/commit/([0-9a-f]{7,40})</id>""", 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) } }