Move GitHub Release/App Update logic to data (#9422)

* Move GitHub Release/App Update logic to data

* Add tests for GetApplicationRelease

* Review changes
This commit is contained in:
Andreas 2023-04-30 04:14:49 +02:00 committed by GitHub
parent eed91f6360
commit 02864ebd60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 425 additions and 157 deletions

View file

@ -20,6 +20,7 @@ import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl
import tachiyomi.data.history.HistoryRepositoryImpl
import tachiyomi.data.manga.MangaRepositoryImpl
import tachiyomi.data.release.ReleaseServiceImpl
import tachiyomi.data.source.SourceDataRepositoryImpl
import tachiyomi.data.source.SourceRepositoryImpl
import tachiyomi.data.track.TrackRepositoryImpl
@ -56,6 +57,8 @@ import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService
import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.repository.SourceDataRepository
@ -102,6 +105,9 @@ class DomainModule : InjektModule {
addFactory { UpdateManga(get()) }
addFactory { SetMangaCategories(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }

View file

@ -31,7 +31,6 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.CrashLogUtil
@ -43,6 +42,7 @@ import logcat.LogPriority
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.LinkIcon
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
@ -186,16 +186,16 @@ object AboutScreen : Screen() {
/**
* Checks version and shows a user prompt if an update is available.
*/
private suspend fun checkVersion(context: Context, onAvailableUpdate: (AppUpdateResult.NewUpdate) -> Unit) {
private suspend fun checkVersion(context: Context, onAvailableUpdate: (GetApplicationRelease.Result.NewUpdate) -> Unit) {
val updateChecker = AppUpdateChecker()
withUIContext {
context.toast(R.string.update_check_look_for_updates)
try {
when (val result = withIOContext { updateChecker.checkForUpdate(context, isUserPrompt = true) }) {
is AppUpdateResult.NewUpdate -> {
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
is GetApplicationRelease.Result.NewUpdate -> {
onAvailableUpdate(result)
}
is AppUpdateResult.NoNewUpdate -> {
is GetApplicationRelease.Result.NoNewUpdate -> {
context.toast(R.string.update_check_no_new_updates)
}
else -> {}

View file

@ -2,92 +2,37 @@ package eu.kanade.tachiyomi.data.updater
import android.content.Context
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import kotlinx.serialization.json.Json
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.release.interactor.GetApplicationRelease
import uy.kohesive.injekt.injectLazy
import java.util.Date
import kotlin.time.Duration.Companion.days
class AppUpdateChecker {
private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
private val json: Json by injectLazy()
private val lastAppCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0)
}
suspend fun checkForUpdate(context: Context, isUserPrompt: Boolean = false): AppUpdateResult {
// Limit checks to once every 3 days at most
if (isUserPrompt.not() && Date().time < lastAppCheck.get() + 3.days.inWholeMilliseconds) {
return AppUpdateResult.NoNewUpdate
}
private val getApplicationRelease: GetApplicationRelease by injectLazy()
suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result {
return withIOContext {
val result = with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.awaitSuccess()
.parseAs<GithubRelease>()
.let {
lastAppCheck.set(Date().time)
// Check if latest version is different from current version
if (isNewVersion(it.version)) {
if (context.isInstalledFromFDroid()) {
AppUpdateResult.NewUpdateFdroidInstallation
} else {
AppUpdateResult.NewUpdate(it)
}
} else {
AppUpdateResult.NoNewUpdate
}
}
}
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
BuildConfig.PREVIEW,
context.isInstalledFromFDroid(),
BuildConfig.COMMIT_COUNT.toInt(),
BuildConfig.VERSION_NAME,
GITHUB_REPO,
forceCheck,
),
)
when (result) {
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
else -> {}
}
result
}
}
private fun isNewVersion(versionTag: String): Boolean {
// Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (BuildConfig.PREVIEW) {
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
// tagged as something like "r1234"
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
} else {
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
// tagged as something like "v0.1.2"
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
val newSemVer = newVersion.split(".").map { it.toInt() }
val oldSemVer = oldVersion.split(".").map { it.toInt() }
oldSemVer.mapIndexed { index, i ->
if (newSemVer[index] > i) {
return true
}
}
false
}
}
}
val GITHUB_REPO: String by lazy {

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.domain.release.model.Release
internal class AppUpdateNotifier(private val context: Context) {
@ -27,18 +28,22 @@ internal class AppUpdateNotifier(private val context: Context) {
context.notify(id, build())
}
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
}
@SuppressLint("LaunchActivityFromNotification")
fun promptUpdate(release: GithubRelease) {
val intent = Intent(context, AppUpdateService::class.java).apply {
fun promptUpdate(release: Release) {
val updateIntent = Intent(context, AppUpdateService::class.java).run {
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version)
PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
val updateIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).apply {
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
PendingIntent.getActivity(context, release.hashCode(), this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
val releaseInfoIntent = PendingIntent.getActivity(context, release.hashCode(), releaseIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.update_check_notification_update_available))
@ -55,7 +60,7 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_info_24dp,
context.getString(R.string.whats_new),
releaseInfoIntent,
releaseIntent,
)
}
notificationBuilder.show()
@ -169,8 +174,4 @@ internal class AppUpdateNotifier(private val context: Context) {
}
notificationBuilder.show(Notifications.ID_APP_UPDATER)
}
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
}
}

View file

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.data.updater
sealed class AppUpdateResult {
class NewUpdate(val release: GithubRelease) : AppUpdateResult()
object NewUpdateFdroidInstallation : AppUpdateResult()
object NoNewUpdate : AppUpdateResult()
}

View file

@ -20,13 +20,13 @@ import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import logcat.LogPriority
import okhttp3.Call
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.File
@ -38,11 +38,10 @@ class AppUpdateService : Service() {
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: AppUpdateNotifier
private var runningJob: Job? = null
private var runningCall: Call? = null
private val job = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
override fun onCreate() {
notifier = AppUpdateNotifier(this)
@ -62,11 +61,11 @@ class AppUpdateService : Service() {
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
runningJob = launchIO {
serviceScope.launch {
downloadApk(title, url)
}
runningJob?.invokeOnCompletion { stopSelf(startId) }
job.invokeOnCompletion { stopSelf(startId) }
return START_NOT_STICKY
}
@ -80,8 +79,8 @@ class AppUpdateService : Service() {
}
private fun destroyJob() {
runningJob?.cancel()
runningCall?.cancel()
serviceScope.cancel()
job.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
@ -116,9 +115,8 @@ class AppUpdateService : Service() {
try {
// Download the new update.
val call = network.client.newCachelessCallWithProgress(GET(url), progressListener)
runningCall = call
val response = call.await()
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
// File where the apk will be saved.
val apkFile = File(externalCacheDir, "update.apk")
@ -131,10 +129,9 @@ class AppUpdateService : Service() {
}
notifier.promptInstall(apkFile.getUriCompat(this))
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
if (e is CancellationException ||
val shouldCancel = e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
) {
if (shouldCancel) {
notifier.cancel()
} else {
notifier.onDownloadError(url)
@ -165,11 +162,11 @@ class AppUpdateService : Service() {
fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
if (isRunning(context)) return
val intent = Intent(context, AppUpdateService::class.java).apply {
Intent(context, AppUpdateService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
ContextCompat.startForegroundService(context, this)
}
ContextCompat.startForegroundService(context, intent)
}
/**
@ -188,10 +185,10 @@ class AppUpdateService : Service() {
* @return [PendingIntent]
*/
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
val intent = Intent(context, AppUpdateService::class.java).apply {
return Intent(context, AppUpdateService::class.java).run {
putExtra(EXTRA_DOWNLOAD_URL, url)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}
}
}

View file

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.os.Build
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Contains information about the latest release from GitHub.
*/
@Serializable
data class GithubRelease(
@SerialName("tag_name") val version: String,
@SerialName("body") val info: String,
@SerialName("html_url") val releaseLink: String,
@SerialName("assets") private val assets: List<Assets>,
) {
/**
* 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.downloadLink.contains("tachiyomi$apkVariant-") }?.downloadLink
?: assets[0].downloadLink
}
/**
* Assets class containing download url.
*/
@Serializable
data class Assets(@SerialName("browser_download_url") val downloadLink: String)
}

View file

@ -70,7 +70,6 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
@ -97,6 +96,7 @@ import logcat.LogPriority
import tachiyomi.core.Constants
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.presentation.core.components.material.Scaffold
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -328,7 +328,7 @@ class MainActivity : BaseActivity() {
if (BuildConfig.INCLUDE_UPDATER) {
try {
val result = AppUpdateChecker().checkForUpdate(context)
if (result is AppUpdateResult.NewUpdate) {
if (result is GetApplicationRelease.Result.NewUpdate) {
val updateScreen = NewUpdateScreen(
versionName = result.release.version,
changelogInfo = result.release.info,

View file

@ -1,6 +1,7 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.serialization")
id("com.squareup.sqldelight")
}
@ -28,3 +29,12 @@ dependencies {
api(libs.sqldelight.coroutines)
api(libs.sqldelight.android.paging)
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}

View file

@ -0,0 +1,31 @@
package tachiyomi.data.release
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import tachiyomi.domain.release.model.Release
/**
* Contains information about the latest release from GitHub.
*/
@Serializable
data class GithubRelease(
@SerialName("tag_name") val version: String,
@SerialName("body") val info: String,
@SerialName("html_url") val releaseLink: String,
@SerialName("assets") val assets: List<GitHubAssets>,
)
/**
* Assets class containing download url.
*/
@Serializable
data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: String)
val releaseMapper: (GithubRelease) -> Release = {
Release(
it.version,
it.info,
it.releaseLink,
it.assets.map(GitHubAssets::downloadLink),
)
}

View file

@ -0,0 +1,25 @@
package tachiyomi.data.release
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import tachiyomi.domain.release.model.Release
import tachiyomi.domain.release.service.ReleaseService
class ReleaseServiceImpl(
private val networkService: NetworkHelper,
private val json: Json,
) : ReleaseService {
override suspend fun latest(repository: String): Release {
return with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/$repository/releases/latest"))
.awaitSuccess()
.parseAs<GithubRelease>()
.let(releaseMapper)
}
}
}

View file

@ -22,4 +22,13 @@ dependencies {
api(libs.sqldelight.android.paging)
testImplementation(libs.bundles.test)
testImplementation(kotlinx.coroutines.test)
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
)
}
}

View file

@ -0,0 +1,79 @@
package tachiyomi.domain.release.interactor
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.release.model.Release
import tachiyomi.domain.release.service.ReleaseService
import java.time.Instant
import java.time.temporal.ChronoUnit
class GetApplicationRelease(
private val service: ReleaseService,
private val preferenceStore: PreferenceStore,
) {
private val lastChecked: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0)
}
suspend fun await(arguments: Arguments): Result {
val now = Instant.now()
// Limit checks to once every 3 days at most
if (arguments.forceCheck.not() && now.isBefore(Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS))) {
return Result.NoNewUpdate
}
val release = service.latest(arguments.repository)
lastChecked.set(now.toEpochMilli())
// Check if latest version is different from current version
val isNewVersion = isNewVersion(arguments.isPreview, arguments.commitCount, arguments.versionName, release.version)
return when {
isNewVersion && arguments.isThirdParty -> Result.ThirdPartyInstallation
isNewVersion -> Result.NewUpdate(release)
else -> Result.NoNewUpdate
}
}
private fun isNewVersion(isPreview: Boolean, commitCount: Int, versionName: String, versionTag: String): Boolean {
// Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (isPreview) {
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
// tagged as something like "r1234"
newVersion.toInt() > commitCount
} else {
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
// tagged as something like "v0.1.2"
val oldVersion = versionName.replace("[^\\d.]".toRegex(), "")
val newSemVer = newVersion.split(".").map { it.toInt() }
val oldSemVer = oldVersion.split(".").map { it.toInt() }
oldSemVer.mapIndexed { index, i ->
if (newSemVer[index] > i) {
return true
}
}
false
}
}
data class Arguments(
val isPreview: Boolean,
val isThirdParty: Boolean,
val commitCount: Int,
val versionName: String,
val repository: String,
val forceCheck: Boolean = false,
)
sealed class Result {
class NewUpdate(val release: Release) : Result()
object NoNewUpdate : Result()
object ThirdPartyInstallation : Result()
}
}

View file

@ -0,0 +1,35 @@
package tachiyomi.domain.release.model
import android.os.Build
/**
* Contains information about the latest release.
*/
data class Release(
val version: String,
val info: String,
val releaseLink: String,
private val assets: List<String>,
) {
/**
* 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("tachiyomi$apkVariant-") } ?: assets[0]
}
/**
* Assets class containing download url.
*/
data class Assets(val downloadLink: String)
}

View file

@ -0,0 +1,8 @@
package tachiyomi.domain.release.service
import tachiyomi.domain.release.model.Release
interface ReleaseService {
suspend fun latest(repository: String): Release
}

View file

@ -0,0 +1,166 @@
package tachiyomi.domain.release.interactor
import io.kotest.matchers.shouldBe
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.release.model.Release
import tachiyomi.domain.release.service.ReleaseService
import java.time.Instant
class GetApplicationReleaseTest {
lateinit var getApplicationRelease: GetApplicationRelease
lateinit var releaseService: ReleaseService
lateinit var preference: Preference<Long>
@BeforeEach
fun beforeEach() {
val preferenceStore = mockk<PreferenceStore>()
preference = mockk()
every { preferenceStore.getLong(any(), any()) } returns preference
releaseService = mockk()
getApplicationRelease = GetApplicationRelease(releaseService, preferenceStore)
}
@Test
fun `When has update but is third party expect third party installation`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
coEvery { releaseService.latest(any()) } returns Release(
"v2.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = true,
commitCount = 0,
versionName = "v1.0.0",
repository = "test",
),
)
result shouldBe GetApplicationRelease.Result.ThirdPartyInstallation
}
@Test
fun `When has update but is preview expect new update`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
val release = Release(
"r2000",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = true,
isThirdParty = false,
commitCount = 1000,
versionName = "",
repository = "test",
),
)
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release
}
@Test
fun `When has update expect new update`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
val release = Release(
"v2.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = false,
commitCount = 0,
versionName = "v1.0.0",
repository = "test",
),
)
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release
}
@Test
fun `When has no update expect no new update`() = runTest {
every { preference.get() } returns 0
every { preference.set(any()) }.answers { }
val release = Release(
"v1.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = false,
commitCount = 0,
versionName = "v2.0.0",
repository = "test",
),
)
result shouldBe GetApplicationRelease.Result.NoNewUpdate
}
@Test
fun `When now is before three days expect no new update`() = runTest {
every { preference.get() } returns Instant.now().toEpochMilli()
every { preference.set(any()) }.answers { }
val release = Release(
"v1.0.0",
"info",
"http://example.com/release_link",
listOf("http://example.com/assets"),
)
coEvery { releaseService.latest(any()) } returns release
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
isPreview = false,
isThirdParty = false,
commitCount = 0,
versionName = "v2.0.0",
repository = "test",
),
)
coVerify(exactly = 0) { releaseService.latest(any()) }
result shouldBe GetApplicationRelease.Result.NoNewUpdate
}
}

View file

@ -11,6 +11,7 @@ coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", vers
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization_version" }

View file

@ -92,6 +92,8 @@ voyager-transitions = { module = "ca.gosyer:voyager-transitions", version.ref =
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
mockk = "io.mockk:mockk:1.13.5"
[bundles]
reactivex = ["rxandroid", "rxjava", "rxrelay"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
@ -101,4 +103,4 @@ coil = ["coil-core", "coil-gif", "coil-compose"]
shizuku = ["shizuku-api", "shizuku-provider"]
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
richtext = ["richtext-commonmark", "richtext-m3"]
test = ["junit", "kotest-assertions"]
test = ["junit", "kotest-assertions", "mockk"]