package com.jttools.datacontrol.data import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.core.content.FileProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import java.io.File import java.net.HttpURLConnection import java.net.URL /** * Mise à jour via les Releases Gitea du dépôt (dernière release publiée + son asset .apk). * Le dépôt est public : aucun jeton n'est nécessaire. */ class UpdateManager(private val context: Context, private val currentVersion: String) { data class UpdateInfo(val versionName: String, val apkUrl: String, val changelog: String) sealed interface CheckResult { data object UpToDate : CheckResult data class Available(val info: UpdateInfo) : CheckResult data class Error(val message: String) : CheckResult } suspend fun check(): CheckResult = withContext(Dispatchers.IO) { try { val conn = open(RELEASES_URL) try { if (conn.responseCode != HttpURLConnection.HTTP_OK) { return@withContext CheckResult.Error("HTTP ${conn.responseCode}") } val json = JSONObject(conn.inputStream.bufferedReader().use { it.readText() }) val version = json.optString("tag_name").removePrefix("v") if (version.isBlank()) return@withContext CheckResult.Error("Release sans tag") val assets = json.optJSONArray("assets") var apkUrl = "" if (assets != null) { for (i in 0 until assets.length()) { val asset = assets.getJSONObject(i) if (asset.optString("name").endsWith(".apk")) { apkUrl = asset.optString("browser_download_url") break } } } when { !isNewer(version, currentVersion) -> CheckResult.UpToDate apkUrl.isBlank() -> CheckResult.Error("Aucun APK dans la release v$version") else -> CheckResult.Available(UpdateInfo(version, apkUrl, json.optString("body"))) } } finally { conn.disconnect() } } catch (e: Exception) { CheckResult.Error(e.message ?: e.javaClass.simpleName) } } /** Télécharge l'APK dans le cache ; [onProgress] reçoit 0-100. Retourne le fichier ou lève une exception. */ suspend fun download(info: UpdateInfo, onProgress: (Int) -> Unit): File = withContext(Dispatchers.IO) { val dir = File(context.cacheDir, "updates").apply { mkdirs() } dir.listFiles()?.forEach { it.delete() } val file = File(dir, "DataControl-${info.versionName}.apk") val conn = open(info.apkUrl) try { if (conn.responseCode != HttpURLConnection.HTTP_OK) error("HTTP ${conn.responseCode}") val total = conn.contentLengthLong var done = 0L conn.inputStream.use { input -> file.outputStream().use { output -> val buf = ByteArray(32 * 1024) var n: Int while (input.read(buf).also { n = it } != -1) { output.write(buf, 0, n) done += n if (total > 0) onProgress((done * 100 / total).toInt()) } } } } finally { conn.disconnect() } file } /** Vrai si l'installation a été lancée ; faux si l'utilisateur doit d'abord autoriser l'installation d'APK. */ fun install(file: File): Boolean { if (!context.packageManager.canRequestPackageInstalls()) { context.startActivity( Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}")) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ) return false } val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) context.startActivity( Intent(Intent.ACTION_VIEW) .setDataAndType(uri, "application/vnd.android.package-archive") .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) ) return true } private fun open(url: String): HttpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply { connectTimeout = 15_000 readTimeout = 30_000 setRequestProperty("User-Agent", "DataControl/$currentVersion") setRequestProperty("Accept", "application/json, application/octet-stream") } companion object { const val RELEASES_URL = "https://git.tuxtech.fr/api/v1/repos/tuxgyver/DataControl/releases/latest" fun isNewer(remote: String, current: String): Boolean { val r = remote.split(".").map { it.toIntOrNull() ?: 0 } val c = current.split(".").map { it.toIntOrNull() ?: 0 } for (i in 0 until maxOf(r.size, c.size)) { val rv = r.getOrElse(i) { 0 } val cv = c.getOrElse(i) { 0 } if (rv != cv) return rv > cv } return false } } }