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" applicationId = "org.maturita.maturita"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 4 versionCode = 5
versionName = "android-4" versionName = "android-5"
buildConfigField("String", "COMMIT", "\"$commit\"") buildConfigField("String", "COMMIT", "\"$commit\"")
buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"") buildConfigField("String", "UPDATE_REPO", "\"pepaskopec-blip/maturita.c\"")
buildConfigField("String", "UPDATE_BRANCH", "\"builds\"") buildConfigField("String", "UPDATE_BRANCH", "\"builds\"")

View file

@ -2,8 +2,11 @@ package org.maturita.maturita.update
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import org.maturita.maturita.BuildConfig import org.maturita.maturita.BuildConfig
import java.io.File import java.io.File
@ -22,6 +25,8 @@ data class UpdateState(
class Updater(private val app: Application) { class Updater(private val app: Application) {
private val io = Executors.newSingleThreadExecutor() private val io = Executors.newSingleThreadExecutor()
private val main = Handler(Looper.getMainLooper()) 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) { private fun emit(cb: (UpdateState) -> Unit, state: UpdateState) {
main.post { cb(state) } main.post { cb(state) }
@ -35,18 +40,17 @@ class Updater(private val app: Application) {
emit(cb, UpdateState(status = "checking", messageKey = "update_checking", messageArg = null)) emit(cb, UpdateState(status = "checking", messageKey = "update_checking", messageArg = null))
io.execute { io.execute {
try { try {
val atom = get("https://github.com/${BuildConfig.UPDATE_REPO}/commits/${BuildConfig.UPDATE_BRANCH}.atom") val atom = get("https://github.com/$repo/commits/$branch.atom")
val sha = Regex("""<id>.*?/commit/([0-9a-f]{7,40})</id>""", RegexOption.IGNORE_CASE) val remote = atomMainSha(atom)
.find(atom)?.groupValues?.get(1) ?: run {
?: throw IllegalStateException("sha") val tip = atomTip(atom) ?: error("sha")
val version = get( get("https://raw.githubusercontent.com/$repo/$tip/VERSION").trim()
"https://raw.githubusercontent.com/${BuildConfig.UPDATE_REPO}/$sha/VERSION", }
).trim()
val local = BuildConfig.COMMIT val local = BuildConfig.COMMIT
if (version.startsWith(local) || local.startsWith(version)) { if (sameCommit(remote, local)) {
emit(cb, UpdateState("ok", version, "update_uptodate", version.take(7))) emit(cb, UpdateState("ok", remote, "update_uptodate", remote.take(7)))
} else { } 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) { } catch (_: Exception) {
emit(cb, UpdateState("error", messageKey = "update_err_network", messageArg = null)) 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)) emit(cb, UpdateState(status = "downloading", messageKey = "update_downloading", messageArg = null))
io.execute { io.execute {
try { try {
val atom = get("https://github.com/${BuildConfig.UPDATE_REPO}/commits/${BuildConfig.UPDATE_BRANCH}.atom") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
val sha = Regex("""<id>.*?/commit/([0-9a-f]{7,40})</id>""", RegexOption.IGNORE_CASE) !app.packageManager.canRequestPackageInstalls()
.find(atom)?.groupValues?.get(1) ) {
?: throw IllegalStateException("sha") val settings = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
val url = "https://raw.githubusercontent.com/${BuildConfig.UPDATE_REPO}/$sha/maturita-android.apk" 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 dir = File(app.cacheDir, "updates").apply { mkdirs() }
val apk = File(dir, "maturita.apk") val apk = File(dir, "maturita.apk")
download(url, 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 flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
} }
main.post { app.startActivity(intent) } 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) { } catch (_: Exception) {
emit(cb, UpdateState("error", messageKey = "update_err_download", messageArg = null)) 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 { private fun isApk(file: File): Boolean {
if (file.length() < 1_000_000L) return false if (file.length() < 1_000_000L) return false
file.inputStream().use { input -> file.inputStream().use { input ->
@ -93,12 +118,18 @@ class Updater(private val app: Application) {
} }
} }
private fun get(url: String): String { private fun open(url: String, readMs: Int): HttpURLConnection {
val c = (URL(url).openConnection() as HttpURLConnection).apply { return (URL(url).openConnection() as HttpURLConnection).apply {
connectTimeout = 15000 connectTimeout = 15000
readTimeout = 15000 readTimeout = readMs
instanceFollowRedirects = true instanceFollowRedirects = true
setRequestProperty("User-Agent", "maturita.c-android")
setRequestProperty("Accept", "*/*")
} }
}
private fun get(url: String): String {
val c = open(url, 15000)
return try { return try {
if (c.responseCode !in 200..299) error("http ${c.responseCode}") if (c.responseCode !in 200..299) error("http ${c.responseCode}")
c.inputStream.bufferedReader().use { it.readText() } c.inputStream.bufferedReader().use { it.readText() }
@ -108,11 +139,7 @@ class Updater(private val app: Application) {
} }
private fun download(url: String, dest: File) { private fun download(url: String, dest: File) {
val c = (URL(url).openConnection() as HttpURLConnection).apply { val c = open(url, 60000)
connectTimeout = 20000
readTimeout = 60000
instanceFollowRedirects = true
}
try { try {
if (c.responseCode !in 200..299) error("http ${c.responseCode}") if (c.responseCode !in 200..299) error("http ${c.responseCode}")
c.inputStream.use { input -> dest.outputStream().use { input.copyTo(it) } } c.inputStream.use { input -> dest.outputStream().use { input.copyTo(it) } }