mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-23 11:38:55 +02:00
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:
@@ -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()) }
|
||||
|
@@ -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 -> {}
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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.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)
|
||||
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.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,
|
||||
|
Reference in New Issue
Block a user