Tweak the app updater logic and add FOSS build support (mihonapp/mihon#1843)

(cherry picked from commit 28093935d867e5d1a95944e67c6d1eb46cab4a37)
This commit is contained in:
AntsyLich 2025-03-18 19:36:16 +06:00 committed by Cuong-Tran
parent ad090eac85
commit cde9a34238
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
11 changed files with 132 additions and 111 deletions

View file

@ -134,7 +134,7 @@ object AboutScreen : Screen() {
versionName = result.release.version, versionName = result.release.version,
changelogInfo = result.release.info, changelogInfo = result.release.info,
releaseLink = result.release.releaseLink, releaseLink = result.release.releaseLink,
downloadLink = result.release.getDownloadLink(), downloadLink = result.release.downloadLink,
) )
navigator.push(updateScreen) navigator.push(updateScreen)
}, },
@ -211,7 +211,7 @@ object AboutScreen : Screen() {
versionName = result.release.version, versionName = result.release.version,
changelogInfo = result.release.info, changelogInfo = result.release.info,
releaseLink = result.release.releaseLink, releaseLink = result.release.releaseLink,
downloadLink = result.release.getDownloadLink(), downloadLink = result.release.downloadLink,
) )
navigator.push(updateScreen) navigator.push(updateScreen)
}, },
@ -352,7 +352,7 @@ object AboutScreen : Screen() {
val updateChecker = AppUpdateChecker() val updateChecker = AppUpdateChecker()
withUIContext { withUIContext {
try { try {
when (val result = withIOContext { updateChecker.getReleaseNotes(context) }) { when (val result = withIOContext { updateChecker.getReleaseNotes() }) {
is GetApplicationRelease.Result.NewUpdate -> { is GetApplicationRelease.Result.NewUpdate -> {
onAvailableUpdate(result) onAvailableUpdate(result)
} }

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.updater
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.system.isFossBuildType
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
@ -34,6 +35,7 @@ class AppUpdateChecker(
return withIOContext { return withIOContext {
val result = getApplicationRelease.await( val result = getApplicationRelease.await(
GetApplicationRelease.Arguments( GetApplicationRelease.Arguments(
isFoss = isFossBuildType,
isPreview = isPreviewBuildType || peekIntoPreview, isPreview = isPreviewBuildType || peekIntoPreview,
commitCount = BuildConfig.COMMIT_COUNT.toInt(), commitCount = BuildConfig.COMMIT_COUNT.toInt(),
versionName = BuildConfig.VERSION_NAME, versionName = BuildConfig.VERSION_NAME,
@ -61,7 +63,7 @@ class AppUpdateChecker(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
AppUpdateDownloadJob.start( AppUpdateDownloadJob.start(
context = context, context = context,
url = result.release.getDownloadLink(), url = result.release.downloadLink,
title = result.release.version, title = result.release.version,
scheduled = true, scheduled = true,
) )
@ -75,10 +77,11 @@ class AppUpdateChecker(
} }
// KMK --> // KMK -->
suspend fun getReleaseNotes(context: Context): GetApplicationRelease.Result { suspend fun getReleaseNotes(): GetApplicationRelease.Result {
return withIOContext { return withIOContext {
getApplicationRelease.awaitReleaseNotes( getApplicationRelease.awaitReleaseNotes(
GetApplicationRelease.Arguments( GetApplicationRelease.Arguments(
isFoss = isFossBuildType,
isPreview = isPreviewBuildType || peekIntoPreview, isPreview = isPreviewBuildType || peekIntoPreview,
commitCount = BuildConfig.COMMIT_COUNT.toInt(), commitCount = BuildConfig.COMMIT_COUNT.toInt(),
versionName = BuildConfig.VERSION_NAME, versionName = BuildConfig.VERSION_NAME,

View file

@ -48,7 +48,7 @@ internal class AppUpdateNotifier(private val context: Context) {
fun promptUpdate(release: Release) { fun promptUpdate(release: Release) {
val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast( val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast(
context, context,
release.getDownloadLink(), release.downloadLink,
release.version, release.version,
) )

View file

@ -448,7 +448,7 @@ class MainActivity : BaseActivity() {
versionName = result.release.version, versionName = result.release.version,
changelogInfo = result.release.info, changelogInfo = result.release.info,
releaseLink = result.release.releaseLink, releaseLink = result.release.releaseLink,
downloadLink = result.release.getDownloadLink(), downloadLink = result.release.downloadLink,
) )
navigator.push(updateScreen) navigator.push(updateScreen)
} }

View file

@ -18,3 +18,6 @@ val isPreviewBuildType: Boolean
val isReleaseBuildType: Boolean val isReleaseBuildType: Boolean
inline get() = BuildConfig.BUILD_TYPE == "release" inline get() = BuildConfig.BUILD_TYPE == "release"
val isFossBuildType: Boolean
inline get() = BuildConfig.BUILD_TYPE == "foss"

View file

@ -2,78 +2,34 @@ package tachiyomi.data.release
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import tachiyomi.domain.release.model.Release
/** /**
* Contains information about the latest release from GitHub. * Contains information about the latest release from GitHub.
*/ */
@Serializable @Serializable
data class GithubRelease( data class GithubRelease(
@SerialName("tag_name") val version: String, @SerialName("tag_name")
@SerialName("body") val info: String, val version: String,
@SerialName("html_url") val releaseLink: String, @SerialName("body")
@SerialName("assets") val assets: List<GitHubAssets>, val info: String,
@SerialName("html_url")
val releaseLink: String,
@SerialName("assets")
val assets: List<GitHubAsset>,
// KMK --> // KMK -->
@SerialName("prerelease") val preRelease: Boolean = false, @SerialName("prerelease")
@SerialName("draft") val draft: Boolean = false, val preRelease: Boolean = false,
@SerialName("draft")
val draft: Boolean = false,
// KMK <-- // KMK <--
) )
/** /**
* Assets class containing download url. * Asset class containing asset name and download url.
*/ */
@Serializable @Serializable
data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: String) data class GitHubAsset(
val name: String,
/** @SerialName("browser_download_url")
* Regular expression that matches a mention to a valid GitHub username, like it's val downloadLink: String,
* done in GitHub Flavored Markdown. It follows these constraints: )
*
* - Alphanumeric with single hyphens (no consecutive hyphens)
* - Cannot begin or end with a hyphen
* - Max length of 39 characters
*
* Convert '(@cuong-tran)' to '([@cuong-tran](https://github.com/cuong-tran))'
*
* Reference: https://stackoverflow.com/a/30281147
*/
val gitHubUsernameMentionRegex =
"""\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))""".toRegex(
RegexOption.IGNORE_CASE,
)
// KMK -->
/**
* Convert from: https://github.com/komikku-app/komikku/compare/23d862d17...48fb4a2e6
* to: [komikku-app/komikku@23d862d17...48fb4a2e6](https://github.com/komikku-app/komikku/compare/23d862d17...48fb4a2e6)
*/
val gitHubCommitsCompareRegex =
"""(\[[^]]+]\()?https://github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/compare/(?<from>[0-9a-f.rv]+)\.\.\.(?<to>[0-9a-f.rv]+)\)?"""
.toRegex(RegexOption.IGNORE_CASE)
// KMK <--
val releaseMapper: (GithubRelease) -> Release = {
Release(
it.version,
it.info
.replace(gitHubUsernameMentionRegex) { mention ->
"[${mention.value}](https://github.com/${mention.value.substring(1)})"
}
// KMK -->
.replace(gitHubCommitsCompareRegex) { matchResult ->
val owner = matchResult.groups["owner"]!!.value
val repo = matchResult.groups["repo"]!!.value
val from = matchResult.groups["from"]!!.value
val to = matchResult.groups["to"]!!.value
"[$owner/$repo@$from...$to](https://github.com/$owner/$repo/compare/$from...$to)"
},
// KMK <--
it.releaseLink,
it.assets.map(GitHubAssets::downloadLink),
// KMK -->
preRelease = it.preRelease,
draft = it.draft,
// KMK <--
)
}

View file

@ -1,10 +1,12 @@
package tachiyomi.data.release package tachiyomi.data.release
import android.os.Build
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.model.Release import tachiyomi.domain.release.model.Release
import tachiyomi.domain.release.service.ReleaseService import tachiyomi.domain.release.service.ReleaseService
@ -13,26 +15,101 @@ class ReleaseServiceImpl(
private val json: Json, private val json: Json,
) : ReleaseService { ) : ReleaseService {
override suspend fun latest(repository: String): Release { override suspend fun latest(arguments: GetApplicationRelease.Arguments): Release? {
return with(json) { val release = with(json) {
networkService.client networkService.client
.newCall(GET("https://api.github.com/repos/$repository/releases/latest")) .newCall(GET("https://api.github.com/repos/${arguments.repository}/releases/latest"))
.awaitSuccess() .awaitSuccess()
.parseAs<GithubRelease>() .parseAs<GithubRelease>()
.let(releaseMapper)
} }
val downloadLink = getDownloadLink(release = release, isFoss = arguments.isFoss) ?: return null
return Release(
version = release.version,
info = release.info.replace(gitHubUsernameMentionRegex) { mention ->
"[${mention.value}](https://github.com/${mention.value.substring(1)})"
},
releaseLink = release.releaseLink,
downloadLink = downloadLink,
)
} }
// KMK --> // KMK -->
override suspend fun releaseNotes(repository: String): List<Release> { override suspend fun releaseNotes(arguments: GetApplicationRelease.Arguments): List<Release> {
val releases = "https://api.github.com/repos/$repository/releases"
return with(json) { return with(json) {
networkService.client networkService.client
.newCall(GET(releases)) .newCall(GET("https://api.github.com/repos/${arguments.repository}/releases/latest"))
.awaitSuccess() .awaitSuccess()
.parseAs<List<GithubRelease>>() .parseAs<List<GithubRelease>>()
.map(releaseMapper) .mapNotNull { release ->
val downloadLink = getDownloadLink(release = release, isFoss = arguments.isFoss) ?: return@mapNotNull null
Release(
version = release.version,
info = release.info.replace(gitHubUsernameMentionRegex) { mention ->
"[${mention.value}](https://github.com/${mention.value.substring(1)})"
}
// KMK -->
.replace(gitHubCommitsCompareRegex) { matchResult ->
val owner = matchResult.groups["owner"]!!.value
val repo = matchResult.groups["repo"]!!.value
val from = matchResult.groups["from"]!!.value
val to = matchResult.groups["to"]!!.value
"[$owner/$repo@$from...$to](https://github.com/$owner/$repo/compare/$from...$to)"
},
// KMK <--
releaseLink = release.releaseLink,
downloadLink = downloadLink,
// KMK -->
preRelease = release.preRelease,
draft = release.draft,
// KMK <--
)
}
} }
} }
// KMK <-- // KMK <--
private fun getDownloadLink(release: GithubRelease, isFoss: Boolean): String? {
val map = release.assets.associate { asset ->
BUILD_TYPES.find { "-$it" in asset.name } to asset.downloadLink
}
return if (!isFoss) {
map[Build.SUPPORTED_ABIS[0]] ?: map[null]
} else {
map[FOSS]
}
}
companion object {
private const val FOSS = "foss"
private val BUILD_TYPES = listOf(FOSS, "arm64-v8a", "armeabi-v7a", "x86_64", "x86")
/**
* Regular expression that matches a mention to a valid GitHub username, like it's
* done in GitHub Flavored Markdown. It follows these constraints:
*
* - Alphanumeric with single hyphens (no consecutive hyphens)
* - Cannot begin or end with a hyphen
* - Max length of 39 characters
*
* Convert '(@cuong-tran)' to '([@cuong-tran](https://github.com/cuong-tran))'
*
* Reference: https://stackoverflow.com/a/30281147
*/
private val gitHubUsernameMentionRegex = """\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))"""
.toRegex(RegexOption.IGNORE_CASE)
// KMK -->
/**
* Convert from: https://github.com/komikku-app/komikku/compare/23d862d17...48fb4a2e6
* to: [komikku-app/komikku@23d862d17...48fb4a2e6](https://github.com/komikku-app/komikku/compare/23d862d17...48fb4a2e6)
*/
private val gitHubCommitsCompareRegex = """(\[[^]]+]\()?https://github\.com/(?<owner>[^/]+)/(?<repo>[^/]+)/compare/(?<from>[0-9a-f.rv]+)\.\.\.(?<to>[0-9a-f.rv]+)\)?"""
.toRegex(RegexOption.IGNORE_CASE)
// KMK <--
}
} }

View file

@ -26,7 +26,7 @@ class GetApplicationRelease(
} }
// KMK --> // KMK -->
val releases = service.releaseNotes(arguments.repository) val releases = service.releaseNotes(arguments)
.filter { .filter {
!it.preRelease && !it.preRelease &&
isNewVersion( isNewVersion(
@ -57,7 +57,7 @@ class GetApplicationRelease(
// KMK --> // KMK -->
suspend fun awaitReleaseNotes(arguments: Arguments): Result { suspend fun awaitReleaseNotes(arguments: Arguments): Result {
val releases = service.releaseNotes(arguments.repository) val releases = service.releaseNotes(arguments)
.filter { !it.preRelease } .filter { !it.preRelease }
val checksumRegex = """---(\R|.)*Checksums(\R|.)*""".toRegex() val checksumRegex = """---(\R|.)*Checksums(\R|.)*""".toRegex()
@ -114,6 +114,7 @@ class GetApplicationRelease(
} }
data class Arguments( data class Arguments(
val isFoss: Boolean,
/** If current version is Preview (beta) build */ /** If current version is Preview (beta) build */
val isPreview: Boolean, val isPreview: Boolean,
/** Commit count of current version */ /** Commit count of current version */

View file

@ -1,7 +1,5 @@
package tachiyomi.domain.release.model package tachiyomi.domain.release.model
import android.os.Build
/** /**
* Contains information about the latest release. * Contains information about the latest release.
*/ */
@ -9,31 +7,9 @@ data class Release(
val version: String, val version: String,
val info: String, val info: String,
val releaseLink: String, val releaseLink: String,
private val assets: List<String>, val downloadLink: String,
// KMK --> // KMK -->
val preRelease: Boolean = false, val preRelease: Boolean = false,
val draft: Boolean = false, val draft: Boolean = false,
// KMK <-- // KMK <--
) { )
/**
* Get download link of latest release from the assets.
* @return download link of latest release.
*/
fun getDownloadLink(): String {
val apkVariant = when (Build.SUPPORTED_ABIS[0]) {
"arm64-v8a" -> "-arm64-v8a"
"armeabi-v7a" -> "-armeabi-v7a"
"x86" -> "-x86"
"x86_64" -> "-x86_64"
else -> ""
}
return assets.find { it.contains("Komikku$apkVariant-") } ?: assets[0]
}
/**
* Assets class containing download url.
*/
data class Assets(val downloadLink: String)
}

View file

@ -1,12 +1,13 @@
package tachiyomi.domain.release.service package tachiyomi.domain.release.service
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.model.Release import tachiyomi.domain.release.model.Release
interface ReleaseService { interface ReleaseService {
suspend fun latest(repository: String): Release suspend fun latest(arguments: GetApplicationRelease.Arguments): Release?
// KMK --> // KMK -->
suspend fun releaseNotes(repository: String): List<Release> suspend fun releaseNotes(arguments: GetApplicationRelease.Arguments): List<Release>
// KMK <-- // KMK <--
} }

View file

@ -40,7 +40,7 @@ class GetApplicationReleaseTest {
"r2000", "r2000",
"info", "info",
"http://example.com/release_link", "http://example.com/release_link",
listOf("http://example.com/assets"), "http://example.com/release_link.apk",
), ),
) )
@ -48,6 +48,7 @@ class GetApplicationReleaseTest {
val result = getApplicationRelease.await( val result = getApplicationRelease.await(
GetApplicationRelease.Arguments( GetApplicationRelease.Arguments(
isFoss = false,
isPreview = true, isPreview = true,
commitCount = 1000, commitCount = 1000,
versionName = "", versionName = "",
@ -70,7 +71,7 @@ class GetApplicationReleaseTest {
"v2.0.0", "v2.0.0",
"info", "info",
"http://example.com/release_link", "http://example.com/release_link",
listOf("http://example.com/assets"), "http://example.com/release_link.apk",
), ),
) )
@ -78,6 +79,7 @@ class GetApplicationReleaseTest {
val result = getApplicationRelease.await( val result = getApplicationRelease.await(
GetApplicationRelease.Arguments( GetApplicationRelease.Arguments(
isFoss = false,
isPreview = false, isPreview = false,
commitCount = 0, commitCount = 0,
versionName = "v1.0.0", versionName = "v1.0.0",
@ -99,7 +101,7 @@ class GetApplicationReleaseTest {
"v1.0.0", "v1.0.0",
"info", "info",
"http://example.com/release_link", "http://example.com/release_link",
listOf("http://example.com/assets"), "http://example.com/release_link.apk",
), ),
) )
@ -107,6 +109,7 @@ class GetApplicationReleaseTest {
val result = getApplicationRelease.await( val result = getApplicationRelease.await(
GetApplicationRelease.Arguments( GetApplicationRelease.Arguments(
isFoss = false,
isPreview = false, isPreview = false,
commitCount = 0, commitCount = 0,
versionName = "v2.0.0", versionName = "v2.0.0",
@ -127,7 +130,7 @@ class GetApplicationReleaseTest {
"v2.0.0", "v2.0.0",
"info", "info",
"http://example.com/release_link", "http://example.com/release_link",
listOf("http://example.com/assets"), "http://example.com/release_link.apk",
), ),
) )
@ -135,6 +138,7 @@ class GetApplicationReleaseTest {
val result = getApplicationRelease.await( val result = getApplicationRelease.await(
GetApplicationRelease.Arguments( GetApplicationRelease.Arguments(
isFoss = false,
isPreview = false, isPreview = false,
commitCount = 0, commitCount = 0,
versionName = "v1.0.0", versionName = "v1.0.0",