Files
DataControl/app/src/main/java/com/jttools/datacontrol/data/UpdateManager.kt
T

132 lines
5.4 KiB
Kotlin

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
}
}
}