Align Android Argus publication with SSH contract
This commit is contained in:
+73
-16
@@ -44,7 +44,7 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) { fetchLatestRelease(context) }
|
||||
}.onSuccess { release ->
|
||||
if (release.androidVersionCode <= BuildConfig.VERSION_CODE) {
|
||||
if (release == null || compareVersions(release.version, BuildConfig.VERSION_NAME) <= 0) {
|
||||
showNoUpdatesDialog(context)
|
||||
} else {
|
||||
showUpdateDialog(context, release)
|
||||
@@ -62,9 +62,7 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
context.getString(
|
||||
R.string.update_available_message,
|
||||
release.version,
|
||||
release.androidVersionCode,
|
||||
BuildConfig.VERSION_NAME,
|
||||
BuildConfig.VERSION_CODE
|
||||
BuildConfig.VERSION_NAME
|
||||
)
|
||||
)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
@@ -95,8 +93,13 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
fragment.lifecycleScope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadApk(context, release).also { apk ->
|
||||
val apk = downloadApk(context, release)
|
||||
try {
|
||||
validateDownloadedApk(context, apk, release)
|
||||
apk
|
||||
} catch (error: Throwable) {
|
||||
apk.delete()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}.onSuccess { apk ->
|
||||
@@ -139,25 +142,45 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun fetchLatestRelease(context: Context): ArgusRelease {
|
||||
val json = requestText(ARGUS_MANIFEST_URL)
|
||||
private fun fetchLatestRelease(context: Context): ArgusRelease? {
|
||||
val json = requestText(ARGUS_MANIFEST_URL) ?: return null
|
||||
val release = JSONObject(json).optJSONObject("release")
|
||||
?: error(context.getString(R.string.update_release_not_found))
|
||||
val version = release.getString("version")
|
||||
val channel = release.optString("channel")
|
||||
val platform = release.optString("platform")
|
||||
val packageKind = release.optString("packageKind")
|
||||
val downloadPath = release.getString("downloadPath")
|
||||
val sha256 = release.optString("sha256")
|
||||
val packageSizeBytes = release.optLong("packageSizeBytes", -1L)
|
||||
|
||||
if (channel != ARGUS_CHANNEL ||
|
||||
platform != ARGUS_PLATFORM ||
|
||||
packageKind != ARGUS_PACKAGE_KIND ||
|
||||
sha256.isBlank() ||
|
||||
packageSizeBytes <= 0L
|
||||
) {
|
||||
error(context.getString(R.string.update_metadata_error))
|
||||
}
|
||||
|
||||
return ArgusRelease(
|
||||
version = release.getString("version"),
|
||||
androidVersionCode = release.getInt("androidVersionCode"),
|
||||
downloadUrl = release.getString("downloadPath").toAbsoluteArgusUrl(),
|
||||
sha256 = release.optString("sha256")
|
||||
version = version,
|
||||
androidVersionCode = release.optInt("androidVersionCode").takeIf { it > 0 },
|
||||
downloadUrl = downloadPath.toAbsoluteArgusUrl(),
|
||||
packageSizeBytes = packageSizeBytes,
|
||||
sha256 = sha256.lowercase()
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestText(url: String): String {
|
||||
private fun requestText(url: String): String? {
|
||||
val connection = URL(url).openConnection() as HttpURLConnection
|
||||
return try {
|
||||
connection.connectTimeout = NETWORK_TIMEOUT_MS
|
||||
connection.readTimeout = NETWORK_TIMEOUT_MS
|
||||
connection.requestMethod = "GET"
|
||||
if (connection.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
return null
|
||||
}
|
||||
check(connection.responseCode in 200..299) {
|
||||
"HTTP ${connection.responseCode}"
|
||||
}
|
||||
@@ -170,6 +193,9 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
private fun downloadApk(context: Context, release: ArgusRelease): File {
|
||||
val updatesDir = File(context.filesDir, "updates").apply { mkdirs() }
|
||||
val apkFile = File(updatesDir, UPDATE_APK_NAME)
|
||||
if (apkFile.exists()) {
|
||||
apkFile.delete()
|
||||
}
|
||||
val connection = URL(release.downloadUrl).openConnection() as HttpURLConnection
|
||||
|
||||
try {
|
||||
@@ -188,7 +214,13 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
connection.disconnect()
|
||||
}
|
||||
|
||||
if (release.sha256.isNotBlank() && apkFile.sha256() != release.sha256.lowercase()) {
|
||||
if (apkFile.length() != release.packageSizeBytes) {
|
||||
apkFile.delete()
|
||||
error(context.getString(R.string.update_metadata_error))
|
||||
}
|
||||
|
||||
if (apkFile.sha256() != release.sha256) {
|
||||
apkFile.delete()
|
||||
error(context.getString(R.string.update_digest_error))
|
||||
}
|
||||
|
||||
@@ -220,7 +252,10 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
if (downloadedVersionCode <= BuildConfig.VERSION_CODE) {
|
||||
error(context.getString(R.string.update_version_error))
|
||||
}
|
||||
if (downloadedVersionCode != release.androidVersionCode.toLong()) {
|
||||
if (release.androidVersionCode != null && downloadedVersionCode != release.androidVersionCode.toLong()) {
|
||||
error(context.getString(R.string.update_metadata_error))
|
||||
}
|
||||
if (downloadedPackageInfo.versionName != release.version) {
|
||||
error(context.getString(R.string.update_metadata_error))
|
||||
}
|
||||
|
||||
@@ -326,10 +361,29 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
private fun compareVersions(candidate: String, installed: String): Int {
|
||||
val candidateParts = candidate.toVersionParts()
|
||||
val installedParts = installed.toVersionParts()
|
||||
val maxParts = maxOf(candidateParts.size, installedParts.size)
|
||||
for (index in 0 until maxParts) {
|
||||
val candidatePart = candidateParts.getOrElse(index) { 0 }
|
||||
val installedPart = installedParts.getOrElse(index) { 0 }
|
||||
if (candidatePart != installedPart) {
|
||||
return candidatePart.compareTo(installedPart)
|
||||
}
|
||||
}
|
||||
return candidate.compareTo(installed, ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun String.toVersionParts(): List<Int> =
|
||||
split('.', '-', '_')
|
||||
.mapNotNull { part -> part.takeWhile { it.isDigit() }.toIntOrNull() }
|
||||
|
||||
private data class ArgusRelease(
|
||||
val version: String,
|
||||
val androidVersionCode: Int,
|
||||
val androidVersionCode: Int?,
|
||||
val downloadUrl: String,
|
||||
val packageSizeBytes: Long,
|
||||
val sha256: String
|
||||
)
|
||||
|
||||
@@ -337,8 +391,11 @@ class AppUpdateManager(private val fragment: Fragment) {
|
||||
|
||||
companion object {
|
||||
private const val ARGUS_BASE_URL = "https://argus.kusoft.xyz"
|
||||
private const val ARGUS_PLATFORM = "android"
|
||||
private const val ARGUS_CHANNEL = "stable"
|
||||
private const val ARGUS_PACKAGE_KIND = "apk"
|
||||
private const val ARGUS_MANIFEST_URL =
|
||||
"$ARGUS_BASE_URL/api/apps/qsfera-mobile/manifest?platform=android&channel=stable"
|
||||
"$ARGUS_BASE_URL/api/apps/qsfera-mobile/manifest?platform=$ARGUS_PLATFORM&channel=$ARGUS_CHANNEL"
|
||||
private const val UPDATE_APK_NAME = "qsfera-update.apk"
|
||||
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
|
||||
private const val NETWORK_TIMEOUT_MS = 15_000
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<string name="update_checking">Проверка обновлений…</string>
|
||||
<string name="update_downloading">Скачивание обновления…</string>
|
||||
<string name="update_available_title">Доступно обновление</string>
|
||||
<string name="update_available_message">Доступна версия %1$s (%2$d). Текущая версия: %3$s (%4$d).</string>
|
||||
<string name="update_available_message">Доступна версия %1$s. Текущая версия: %2$s.</string>
|
||||
<string name="update_not_available_title">Обновлений нет</string>
|
||||
<string name="update_not_available_message">Установлена последняя доступная версия.</string>
|
||||
<string name="update_install">Установить</string>
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<string name="update_checking">Checking for updates…</string>
|
||||
<string name="update_downloading">Downloading update…</string>
|
||||
<string name="update_available_title">Update available</string>
|
||||
<string name="update_available_message">Version %1$s (%2$d) is available. Current version: %3$s (%4$d).</string>
|
||||
<string name="update_available_message">Version %1$s is available. Current version: %2$s.</string>
|
||||
<string name="update_not_available_title">No updates</string>
|
||||
<string name="update_not_available_message">You are using the latest available version.</string>
|
||||
<string name="update_install">Install</string>
|
||||
|
||||
Reference in New Issue
Block a user