All checks were successful
release / build-release (push) Successful in 5m26s
- CI publicerar version.json (versionCode/versionName ur APK:n, sha256, apkUrl) som release-asset - Appen jämför versionCode vid start; banner på hemskärmen med nedladdningsprogress; sha256-verifiering; Androids installationsdialog via FileProvider (samma signatur -> uppdaterar rakt över) - REQUEST_INSTALL_PACKAGES + FileProvider i manifestet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
110 lines
4.5 KiB
Kotlin
110 lines
4.5 KiB
Kotlin
package eu.brassepc.fitnessdroid.data
|
|
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import androidx.core.content.FileProvider
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.withContext
|
|
import kotlinx.serialization.json.Json
|
|
import kotlinx.serialization.json.intOrNull
|
|
import kotlinx.serialization.json.jsonObject
|
|
import kotlinx.serialization.json.jsonPrimitive
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import java.io.File
|
|
import java.security.MessageDigest
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
data class UpdateInfo(
|
|
val versionCode: Long,
|
|
val versionName: String,
|
|
val apkUrl: String,
|
|
val sha256: String?,
|
|
)
|
|
|
|
/**
|
|
* Uppdateringskoll mot släppsidans version.json (skrivs av CI:t).
|
|
* Nedladdning till appens cache + verifiering, sedan Androids
|
|
* installationsdialog — samma signeringsnyckel gör att den uppdaterar
|
|
* rakt över befintlig installation.
|
|
*/
|
|
class UpdateChecker(private val context: Context) {
|
|
|
|
private val http = OkHttpClient.Builder()
|
|
.connectTimeout(10, TimeUnit.SECONDS)
|
|
.readTimeout(120, TimeUnit.SECONDS)
|
|
.build()
|
|
|
|
private fun installedVersionCode(): Long =
|
|
context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode
|
|
|
|
/** null = uppdaterad, nätfel eller trasig manifest — stör aldrig användaren. */
|
|
suspend fun check(): UpdateInfo? = withContext(Dispatchers.IO) {
|
|
runCatching {
|
|
val request = Request.Builder().url(VERSION_URL).build()
|
|
http.newCall(request).execute().use { response ->
|
|
if (!response.isSuccessful) return@withContext null
|
|
val o = Json.parseToJsonElement(response.body!!.string()).jsonObject
|
|
val info = UpdateInfo(
|
|
versionCode = (o["versionCode"]?.jsonPrimitive?.intOrNull ?: 0).toLong(),
|
|
versionName = o["versionName"]?.jsonPrimitive?.content ?: "?",
|
|
apkUrl = o["apkUrl"]?.jsonPrimitive?.content ?: return@withContext null,
|
|
sha256 = o["sha256"]?.jsonPrimitive?.content,
|
|
)
|
|
if (info.versionCode > installedVersionCode()) info else null
|
|
}
|
|
}.getOrNull()
|
|
}
|
|
|
|
/** Laddar ner APK:n till cachen, verifierar sha256, returnerar filen. */
|
|
suspend fun download(info: UpdateInfo, onProgress: (Float) -> Unit): File =
|
|
withContext(Dispatchers.IO) {
|
|
val dir = File(context.cacheDir, "updates").apply { mkdirs() }
|
|
val file = File(dir, "FitnessDroid-${info.versionCode}.apk")
|
|
val request = Request.Builder().url(info.apkUrl).build()
|
|
http.newCall(request).execute().use { response ->
|
|
if (!response.isSuccessful) error("HTTP ${response.code}")
|
|
val body = response.body!!
|
|
val total = body.contentLength().takeIf { it > 0 }
|
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
body.byteStream().use { input ->
|
|
file.outputStream().use { output ->
|
|
val buffer = ByteArray(64 * 1024)
|
|
var read: Int
|
|
var done = 0L
|
|
while (input.read(buffer).also { read = it } != -1) {
|
|
output.write(buffer, 0, read)
|
|
digest.update(buffer, 0, read)
|
|
done += read
|
|
total?.let { onProgress(done.toFloat() / it) }
|
|
}
|
|
}
|
|
}
|
|
val actual = digest.digest().joinToString("") { "%02x".format(it) }
|
|
if (info.sha256 != null && !actual.equals(info.sha256, ignoreCase = true)) {
|
|
file.delete()
|
|
error("sha256 stämmer inte — avbryter")
|
|
}
|
|
}
|
|
file
|
|
}
|
|
|
|
/** Öppnar Androids installationsdialog för den nedladdade APK:n. */
|
|
fun install(file: File) {
|
|
val uri = FileProvider.getUriForFile(
|
|
context, "${context.packageName}.fileprovider", file,
|
|
)
|
|
context.startActivity(
|
|
Intent(Intent.ACTION_VIEW).apply {
|
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
}
|
|
)
|
|
}
|
|
|
|
companion object {
|
|
private const val VERSION_URL =
|
|
"https://gitea.brasse-pc.eu/brasse/FitnessDroid/releases/download/latest/version.json"
|
|
}
|
|
}
|