android, desktop: using SemVer when checking for updates (#4768)

* android, desktop: using SemVer when checking for updates

* simplify

* simplify

* no comment

* simplify

* change

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-08-26 20:06:21 +00:00
committed by GitHub
parent 0118e64ab4
commit f1e8c65aa1
2 changed files with 145 additions and 12 deletions
@@ -19,12 +19,76 @@ import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.Closeable
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import kotlin.math.min
data class SemVer(
val major: Int,
val minor: Int,
val patch: Int,
val preRelease: String? = null,
val buildNumber: Int? = null,
): Comparable<SemVer?> {
val isNotStable: Boolean = preRelease != null
override fun compareTo(other: SemVer?): Int {
if (other == null) return 1
return when {
major != other.major -> major.compareTo(other.major)
minor != other.minor -> minor.compareTo(other.minor)
patch != other.patch -> patch.compareTo(other.patch)
preRelease != null && other.preRelease != null -> {
val pr = preRelease.compareTo(other.preRelease, ignoreCase = true)
when {
pr != 0 -> pr
buildNumber != null && other.buildNumber != null -> buildNumber.compareTo(other.buildNumber)
buildNumber != null -> -1
other.buildNumber != null -> 1
else -> 0
}
}
preRelease != null -> -1
other.preRelease != null -> 1
else -> 0
}
}
companion object {
private val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([A-Za-z]+)\\.(\\d+))?\$")
fun from(tagName: String): SemVer? {
val trimmed = tagName.trimStart { it == 'v' }
val redacted = when {
trimmed.contains('-') && trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed.substringBefore('-')}.0-${trimmed.substringAfter('-')}"
trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed}.0"
else -> trimmed
}
val group = regex.matchEntire(redacted)?.groups
return if (group != null) {
SemVer(
major = group[1]?.value?.toIntOrNull() ?: return null,
minor = group[2]?.value?.toIntOrNull() ?: return null,
patch = group[3]?.value?.toIntOrNull() ?: return null,
preRelease = group[4]?.value,
buildNumber = group[5]?.value?.toIntOrNull(),
)
} else {
null
}
}
fun fromCurrentVersionName(): SemVer? {
val currentVersionName = if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME
return from(currentVersionName)
}
}
}
@Serializable
data class GitHubRelease(
@@ -34,12 +98,18 @@ data class GitHubRelease(
val htmlUrl: String,
val name: String,
val draft: Boolean,
val prerelease: Boolean,
@SerialName("prerelease")
private val preRelease: Boolean,
val body: String,
@SerialName("published_at")
val publishedAt: String,
val assets: List<GitHubAsset>
)
) {
@Transient
val semVer: SemVer? = SemVer.from(tagName)
val isConsideredBeta: Boolean = preRelease || semVer == null || semVer.isNotStable
}
@Serializable
data class GitHubAsset(
@@ -105,25 +175,25 @@ private fun createUpdateJob() {
fun checkForUpdate() {
Log.d(TAG, "Checking for update")
val currentSemVer = SemVer.fromCurrentVersionName()
if (currentSemVer == null) {
Log.e(TAG, "Current SemVer cannot be parsed")
return
}
val client = setupHttpClient()
try {
val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build()
client.newCall(request).execute().use { response ->
response.body?.use {
val body = it.string()
val releases = json.decodeFromString<List<GitHubRelease>>(body).filterNot { it.draft }
val releases = json.decodeFromString<List<GitHubRelease>>(body)
val release = when (appPrefs.appUpdateChannel.get()) {
AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease }
AppUpdatesChannel.BETA -> releases.firstOrNull()
AppUpdatesChannel.STABLE -> releases.firstOrNull { r -> !r.draft && !r.isConsideredBeta && currentSemVer < r.semVer }
AppUpdatesChannel.BETA -> releases.firstOrNull { r -> !r.draft && currentSemVer < r.semVer }
AppUpdatesChannel.DISABLED -> return
} ?: return
val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME)
val redactedCurrentVersionName = when {
currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}"
currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0"
else -> currentVersionName
}
if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) {
if (release == null || release.tagName == appPrefs.appSkippedUpdate.get()) {
Log.d(TAG, "Skipping update because of the same version or skipped version")
return
}
@@ -0,0 +1,63 @@
package chat.simplex.app
import chat.simplex.common.views.helpers.SemVer
import kotlin.test.Test
import kotlin.test.assertEquals
// use this command for testing:
// ./gradlew desktopTest
class SemVerTest {
@Test
fun testValidSemVer() {
assertEquals(SemVer.from("1.0.0"), SemVer(1, 0, 0))
assertEquals(SemVer.from("1.0"), SemVer(1, 0, 0))
assertEquals(SemVer.from("v1.0"), SemVer(1, 0, 0))
assertEquals(SemVer.from("v1.0-beta.1"), SemVer(1, 0, 0, "beta", 1))
val r = listOf<Pair<String, SemVer>>(
"0.0.4" to SemVer(0, 0, 4),
"1.2.3" to SemVer(1, 2, 3),
"10.20.30" to SemVer(10, 20, 30),
"1.0.0-alpha.1" to SemVer(1, 0, 0, "alpha", buildNumber = 1),
"1.0.0" to SemVer(1, 0, 0),
"2.0.0" to SemVer(2, 0, 0),
"1.1.7" to SemVer(1, 1, 7),
"2.0.1-alpha.1227" to SemVer(2, 0, 1, "alpha", 1227),
)
r.forEach { (value, correct) ->
assertEquals(SemVer.from(value), correct)
}
}
@Test
fun testComparisonSemVer() {
assert(SemVer(0, 1, 0) == SemVer.from("0.1.0"))
assert(SemVer(1, 1, 0) == SemVer.from("v1.1.0"))
assert(SemVer(0, 1, 0) > SemVer(0, 0, 1))
assert(SemVer(1, 0, 0) > SemVer(0, 100, 100))
assert(SemVer(0, 200, 0) > SemVer(0, 100, 100))
assert(SemVer(0, 1, 0, "beta") > SemVer(0, 1, 0, "alpha"))
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "alpha"))
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta"))
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta.0"))
assert(SemVer(0, 1, 0, "beta", 1) > SemVer(0, 1, 0, "beta", 0))
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 10))
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 9))
assert(SemVer(0, 1, 0, "beta.1") > SemVer(0, 1, 0, "alpha.2"))
assert(SemVer(1, 1, 0, "beta.1") > SemVer(0, 1, 0, "beta.1"))
assert(SemVer(1, 0, 0) > SemVer(1, 0, 0, "beta.1"))
assert(SemVer(1, 0, 0) > null)
assert(SemVer.from("v6.0.0")!! > SemVer.from("v6.0.0-beta.3"))
assert(SemVer.from("v6.0.0-beta.3")!! > SemVer.from("v6.0.0-beta.2"))
assert(SemVer.from("0.1.0") == SemVer.from("0.1.0"))
assert(SemVer.from("0.1.1")!! > SemVer.from("0.1.0"))
assert(SemVer.from("0.2.1")!! > SemVer.from("0.1.1"))
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
assert(SemVer.from("0.1.1-beta.0")!! > SemVer.from("0.1.0-beta.0"))
assert(SemVer.from("0.1.1-beta.0")!! == SemVer.from("0.1.1-beta.0"))
assert(SemVer.from("0.1.1-beta.1")!! > SemVer.from("0.1.1-beta.0"))
assert(SemVer.from("10.0.0-beta.12")!! > SemVer.from("1.1.1"))
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.9"))
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.120"))
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
}
}