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:
parent
eed91f6360
commit
02864ebd60
@ -20,6 +20,7 @@ import tachiyomi.data.category.CategoryRepositoryImpl
|
|||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
import tachiyomi.data.manga.MangaRepositoryImpl
|
||||||
|
import tachiyomi.data.release.ReleaseServiceImpl
|
||||||
import tachiyomi.data.source.SourceDataRepositoryImpl
|
import tachiyomi.data.source.SourceDataRepositoryImpl
|
||||||
import tachiyomi.data.source.SourceRepositoryImpl
|
import tachiyomi.data.source.SourceRepositoryImpl
|
||||||
import tachiyomi.data.track.TrackRepositoryImpl
|
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.ResetViewerFlags
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
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.GetRemoteManga
|
||||||
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||||
import tachiyomi.domain.source.repository.SourceDataRepository
|
import tachiyomi.domain.source.repository.SourceDataRepository
|
||||||
@ -102,6 +105,9 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { UpdateManga(get()) }
|
addFactory { UpdateManga(get()) }
|
||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
|
|
||||||
|
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||||
|
addFactory { GetApplicationRelease(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
||||||
addFactory { DeleteTrack(get()) }
|
addFactory { DeleteTrack(get()) }
|
||||||
addFactory { GetTracksPerManga(get()) }
|
addFactory { GetTracksPerManga(get()) }
|
||||||
|
@ -31,7 +31,6 @@ import eu.kanade.presentation.util.Screen
|
|||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
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.data.updater.RELEASE_URL
|
||||||
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
|
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
|
||||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||||
@ -43,6 +42,7 @@ import logcat.LogPriority
|
|||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import tachiyomi.presentation.core.components.LinkIcon
|
import tachiyomi.presentation.core.components.LinkIcon
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
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.
|
* 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()
|
val updateChecker = AppUpdateChecker()
|
||||||
withUIContext {
|
withUIContext {
|
||||||
context.toast(R.string.update_check_look_for_updates)
|
context.toast(R.string.update_check_look_for_updates)
|
||||||
try {
|
try {
|
||||||
when (val result = withIOContext { updateChecker.checkForUpdate(context, isUserPrompt = true) }) {
|
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
|
||||||
is AppUpdateResult.NewUpdate -> {
|
is GetApplicationRelease.Result.NewUpdate -> {
|
||||||
onAvailableUpdate(result)
|
onAvailableUpdate(result)
|
||||||
}
|
}
|
||||||
is AppUpdateResult.NoNewUpdate -> {
|
is GetApplicationRelease.Result.NoNewUpdate -> {
|
||||||
context.toast(R.string.update_check_no_new_updates)
|
context.toast(R.string.update_check_no_new_updates)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
|
@ -2,92 +2,37 @@ package eu.kanade.tachiyomi.data.updater
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
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 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.core.util.lang.withIOContext
|
||||||
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
|
||||||
import kotlin.time.Duration.Companion.days
|
|
||||||
|
|
||||||
class AppUpdateChecker {
|
class AppUpdateChecker {
|
||||||
|
|
||||||
private val networkService: NetworkHelper by injectLazy()
|
private val getApplicationRelease: GetApplicationRelease 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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val result = with(json) {
|
val result = getApplicationRelease.await(
|
||||||
networkService.client
|
GetApplicationRelease.Arguments(
|
||||||
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
|
BuildConfig.PREVIEW,
|
||||||
.awaitSuccess()
|
context.isInstalledFromFDroid(),
|
||||||
.parseAs<GithubRelease>()
|
BuildConfig.COMMIT_COUNT.toInt(),
|
||||||
.let {
|
BuildConfig.VERSION_NAME,
|
||||||
lastAppCheck.set(Date().time)
|
GITHUB_REPO,
|
||||||
|
forceCheck,
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
||||||
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
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 {
|
val GITHUB_REPO: String by lazy {
|
||||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notify
|
import eu.kanade.tachiyomi.util.system.notify
|
||||||
|
import tachiyomi.domain.release.model.Release
|
||||||
|
|
||||||
internal class AppUpdateNotifier(private val context: Context) {
|
internal class AppUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
@ -27,18 +28,22 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||||||
context.notify(id, build())
|
context.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("LaunchActivityFromNotification")
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
fun promptUpdate(release: GithubRelease) {
|
fun promptUpdate(release: Release) {
|
||||||
val intent = Intent(context, AppUpdateService::class.java).apply {
|
val updateIntent = Intent(context, AppUpdateService::class.java).run {
|
||||||
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
|
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
|
||||||
putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version)
|
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
|
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) {
|
with(notificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.update_check_notification_update_available))
|
setContentTitle(context.getString(R.string.update_check_notification_update_available))
|
||||||
@ -55,7 +60,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_info_24dp,
|
R.drawable.ic_info_24dp,
|
||||||
context.getString(R.string.whats_new),
|
context.getString(R.string.whats_new),
|
||||||
releaseInfoIntent,
|
releaseIntent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
notificationBuilder.show()
|
notificationBuilder.show()
|
||||||
@ -169,8 +174,4 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
notificationBuilder.show(Notifications.ID_APP_UPDATER)
|
notificationBuilder.show(Notifications.ID_APP_UPDATER)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
|
||||||
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
@ -20,13 +20,13 @@ import eu.kanade.tachiyomi.util.storage.saveTo
|
|||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import logcat.LogPriority
|
import kotlinx.coroutines.Dispatchers
|
||||||
import okhttp3.Call
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.internal.http2.ErrorCode
|
import okhttp3.internal.http2.ErrorCode
|
||||||
import okhttp3.internal.http2.StreamResetException
|
import okhttp3.internal.http2.StreamResetException
|
||||||
import tachiyomi.core.util.lang.launchIO
|
|
||||||
import tachiyomi.core.util.system.logcat
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -38,11 +38,10 @@ class AppUpdateService : Service() {
|
|||||||
* Wake lock that will be held until the service is destroyed.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
private lateinit var notifier: AppUpdateNotifier
|
private lateinit var notifier: AppUpdateNotifier
|
||||||
|
|
||||||
private var runningJob: Job? = null
|
private val job = SupervisorJob()
|
||||||
private var runningCall: Call? = null
|
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
notifier = AppUpdateNotifier(this)
|
notifier = AppUpdateNotifier(this)
|
||||||
@ -62,11 +61,11 @@ class AppUpdateService : Service() {
|
|||||||
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
|
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
|
||||||
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
|
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
|
||||||
|
|
||||||
runningJob = launchIO {
|
serviceScope.launch {
|
||||||
downloadApk(title, url)
|
downloadApk(title, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
runningJob?.invokeOnCompletion { stopSelf(startId) }
|
job.invokeOnCompletion { stopSelf(startId) }
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +79,8 @@ class AppUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
runningJob?.cancel()
|
serviceScope.cancel()
|
||||||
runningCall?.cancel()
|
job.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -116,9 +115,8 @@ class AppUpdateService : Service() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Download the new update.
|
// Download the new update.
|
||||||
val call = network.client.newCachelessCallWithProgress(GET(url), progressListener)
|
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
|
||||||
runningCall = call
|
.await()
|
||||||
val response = call.await()
|
|
||||||
|
|
||||||
// File where the apk will be saved.
|
// File where the apk will be saved.
|
||||||
val apkFile = File(externalCacheDir, "update.apk")
|
val apkFile = File(externalCacheDir, "update.apk")
|
||||||
@ -131,10 +129,9 @@ class AppUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
notifier.promptInstall(apkFile.getUriCompat(this))
|
notifier.promptInstall(apkFile.getUriCompat(this))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
val shouldCancel = e is CancellationException ||
|
||||||
if (e is CancellationException ||
|
|
||||||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
|
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
|
||||||
) {
|
if (shouldCancel) {
|
||||||
notifier.cancel()
|
notifier.cancel()
|
||||||
} else {
|
} else {
|
||||||
notifier.onDownloadError(url)
|
notifier.onDownloadError(url)
|
||||||
@ -165,11 +162,11 @@ class AppUpdateService : Service() {
|
|||||||
fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
|
fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
|
||||||
if (isRunning(context)) return
|
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_TITLE, title)
|
||||||
putExtra(EXTRA_DOWNLOAD_URL, url)
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
|
ContextCompat.startForegroundService(context, this)
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -188,10 +185,10 @@ class AppUpdateService : Service() {
|
|||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun downloadApkPendingService(context: Context, url: String): 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)
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
}
|
PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -70,7 +70,6 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
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.data.updater.RELEASE_URL
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
@ -97,6 +96,7 @@ import logcat.LogPriority
|
|||||||
import tachiyomi.core.Constants
|
import tachiyomi.core.Constants
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -328,7 +328,7 @@ class MainActivity : BaseActivity() {
|
|||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
try {
|
try {
|
||||||
val result = AppUpdateChecker().checkForUpdate(context)
|
val result = AppUpdateChecker().checkForUpdate(context)
|
||||||
if (result is AppUpdateResult.NewUpdate) {
|
if (result is GetApplicationRelease.Result.NewUpdate) {
|
||||||
val updateScreen = NewUpdateScreen(
|
val updateScreen = NewUpdateScreen(
|
||||||
versionName = result.release.version,
|
versionName = result.release.version,
|
||||||
changelogInfo = result.release.info,
|
changelogInfo = result.release.info,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
|
kotlin("plugin.serialization")
|
||||||
id("com.squareup.sqldelight")
|
id("com.squareup.sqldelight")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,3 +29,12 @@ dependencies {
|
|||||||
api(libs.sqldelight.coroutines)
|
api(libs.sqldelight.coroutines)
|
||||||
api(libs.sqldelight.android.paging)
|
api(libs.sqldelight.android.paging)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
|
"-Xcontext-receivers",
|
||||||
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
31
data/src/main/java/tachiyomi/data/release/GithubRelease.kt
Normal file
31
data/src/main/java/tachiyomi/data/release/GithubRelease.kt
Normal 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),
|
||||||
|
)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,4 +22,13 @@ dependencies {
|
|||||||
api(libs.sqldelight.android.paging)
|
api(libs.sqldelight.android.paging)
|
||||||
|
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
|
testImplementation(kotlinx.coroutines.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package tachiyomi.domain.release.service
|
||||||
|
|
||||||
|
import tachiyomi.domain.release.model.Release
|
||||||
|
|
||||||
|
interface ReleaseService {
|
||||||
|
|
||||||
|
suspend fun latest(repository: String): Release
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", vers
|
|||||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
||||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
|
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
|
||||||
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
|
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 = { 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" }
|
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization_version" }
|
||||||
|
@ -92,6 +92,8 @@ voyager-transitions = { module = "ca.gosyer:voyager-transitions", version.ref =
|
|||||||
|
|
||||||
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
||||||
|
|
||||||
|
mockk = "io.mockk:mockk:1.13.5"
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
reactivex = ["rxandroid", "rxjava", "rxrelay"]
|
reactivex = ["rxandroid", "rxjava", "rxrelay"]
|
||||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
||||||
@ -101,4 +103,4 @@ coil = ["coil-core", "coil-gif", "coil-compose"]
|
|||||||
shizuku = ["shizuku-api", "shizuku-provider"]
|
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||||
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
|
voyager = ["voyager-navigator", "voyager-tab-navigator", "voyager-transitions"]
|
||||||
richtext = ["richtext-commonmark", "richtext-m3"]
|
richtext = ["richtext-commonmark", "richtext-m3"]
|
||||||
test = ["junit", "kotest-assertions"]
|
test = ["junit", "kotest-assertions", "mockk"]
|
Loading…
Reference in New Issue
Block a user