Download app update without WorkManager (#3707)

This commit is contained in:
AntsyLich
2026-08-06 21:38:07 +06:00
committed by GitHub
parent 869fa1a4b2
commit aa25b3a14c
4 changed files with 57 additions and 198 deletions
@@ -65,11 +65,9 @@ object Notifications {
const val ID_INCOGNITO_MODE = -701
/**
* Notification channel and ids used for app and extension updates.
* Notification channel and ids used for extension updates.
*/
private const val GROUP_APK_UPDATES = "group_apk_updates"
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
const val ID_APP_UPDATER = 1
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
const val ID_UPDATES_TO_EXTS = -401
const val ID_EXTENSION_INSTALLER = -402
@@ -84,6 +82,7 @@ object Notifications {
"downloader_cache_renewal",
"crash_logs_channel",
"library_skipped_channel",
"app_apk_update_channel",
)
/**
@@ -157,10 +156,6 @@ object Notifications {
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
setName(context.stringResource(MR.strings.pref_incognito_mode))
},
buildNotificationChannel(CHANNEL_APP_UPDATE, IMPORTANCE_DEFAULT) {
setGroup(GROUP_APK_UPDATES)
setName(context.stringResource(MR.strings.channel_app_updates))
},
buildNotificationChannel(CHANNEL_EXTENSIONS_UPDATE, IMPORTANCE_DEFAULT) {
setGroup(GROUP_APK_UPDATES)
setName(context.stringResource(MR.strings.channel_ext_updates))
@@ -1,131 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.io.File
class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val network: NetworkHelper by injectLazy()
override suspend fun doWork(): Result {
val url = inputData.getString(EXTRA_DOWNLOAD_URL)
if (url.isNullOrEmpty()) {
return Result.failure()
}
setForegroundSafely()
return try {
withIOContext { downloadApk(url) }
Result.success(workDataOf(PROGRESS to 100, EXTRA_DOWNLOAD_URL to url))
} catch (e: CancellationException) {
throw e
} catch (_: Exception) {
Result.failure()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE)
.setContentTitle(context.stringResource(MR.strings.update_check_notification_update_available))
.setContentText(context.stringResource(MR.strings.update_check_notification_download_in_progress))
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.build()
return ForegroundInfo(
Notifications.ID_APP_UPDATER,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
}
/**
* Called to start downloading apk of new update
*
* @param url url location of file
*/
private suspend fun downloadApk(url: String) {
val progressListener = object : ProgressListener {
// Progress of the download
var savedProgress = 0
// Keep track of the last notification sent to avoid posting too many.
var lastTick = 0L
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt()
val currentTime = System.currentTimeMillis()
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
setProgressAsync(workDataOf(PROGRESS to progress, EXTRA_DOWNLOAD_URL to url))
}
}
}
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener).awaitSuccess()
val apkFile = updateApk(context)
response.body.source().saveTo(apkFile)
}
companion object {
const val TAG = "AppUpdateDownload"
const val PROGRESS = "progress"
const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL"
fun updateApk(context: Context): File = File(context.externalCacheDir, "update.apk")
fun start(context: Context, url: String) {
val constraints = Constraints(
requiredNetworkType = NetworkType.CONNECTED,
)
val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>()
.setConstraints(constraints)
.addTag(TAG)
.setInputData(workDataOf(EXTRA_DOWNLOAD_URL to url))
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
}
fun stop(context: Context) {
context.workManager.cancelUniqueWork(TAG)
}
}
}
@@ -7,79 +7,80 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.work.WorkInfo
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import eu.kanade.tachiyomi.util.storage.saveTo
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.core.viewmodel.StateViewModel
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class NewUpdateScreenModel(
changelogInfo: String,
private val downloadLink: String,
private val context: Context = Injekt.get(),
private val network: NetworkHelper = Injekt.get(),
) : StateViewModel<NewUpdateScreenModel.State>(State(changelogInfo = changelogInfo)) {
init {
context.workManager.getWorkInfosByTagFlow(AppUpdateDownloadJob.TAG)
.mapNotNull { it.firstOrNull() }
.map { workInfo ->
val progress = if (workInfo.state.isFinished) {
workInfo.outputData.getInt(AppUpdateDownloadJob.PROGRESS, 0)
} else {
workInfo.progress.getInt(AppUpdateDownloadJob.PROGRESS, 0)
}
val url = if (workInfo.state.isFinished) {
workInfo.outputData.getString(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL)
} else {
workInfo.progress.getString(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL)
}
private val apkFile: File
get() = File(context.externalCacheDir, "update.apk")
if (url != downloadLink) {
return@map 0 to Stage.Available
}
val stage = when {
workInfo.state == WorkInfo.State.FAILED -> Stage.Failed
workInfo.state.isFinished && progress == 100 -> {
if (AppUpdateDownloadJob.updateApk(context).exists()) {
Stage.Downloaded
} else {
Stage.Available
}
}
workInfo.state in listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING) -> Stage.Downloading
else -> Stage.Available
}
progress to stage
}
.distinctUntilChanged()
.onEach { (progress, stage) ->
mutableState.update {
it.copy(
downloadProgress = progress,
stage = stage,
)
}
}
.launchIn(viewModelScope)
}
private var downloadJob: Job? = null
fun startDownload() {
if (downloadJob?.isActive == true) return
downloadJob = viewModelScope.launch {
mutableState.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) }
AppUpdateDownloadJob.start(context, downloadLink)
try {
withIOContext { downloadApk() }
mutableState.update { it.copy(downloadProgress = 100, stage = Stage.Downloaded) }
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
apkFile.delete()
mutableState.update { it.copy(stage = Stage.Failed) }
}
}
}
private suspend fun downloadApk() {
val progressListener = object : ProgressListener {
// Progress of the download
var savedProgress = 0
// Keep track of the last update sent to avoid updating the state too often.
var lastTick = 0L
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt()
val currentTime = System.currentTimeMillis()
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
mutableState.update { it.copy(downloadProgress = progress) }
}
}
}
val response = network.client.newCachelessCallWithProgress(GET(downloadLink), progressListener).awaitSuccess()
response.body.source().saveTo(apkFile)
}
fun installUpdate() {
val apkFile = AppUpdateDownloadJob.updateApk(context)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(apkFile.getUriCompat(context), ExtensionInstaller.APK_MIME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
@@ -87,10 +88,6 @@ class NewUpdateScreenModel(
context.startActivity(intent)
}
override fun onCleared() {
AppUpdateDownloadJob.stop(context)
}
@Immutable
data class State(
val changelogInfo: String,
@@ -951,7 +951,6 @@
<string name="update_check_no_new_updates">No new updates available</string>
<!--UpdateCheck Notifications-->
<string name="update_check_notification_download_in_progress">Downloading…</string>
<string name="update_check_notification_update_available">New version available!</string>
<!-- Information Text -->
@@ -990,7 +989,6 @@
<string name="channel_complete">Complete</string>
<string name="channel_errors">Errors</string>
<string name="channel_new_chapters">Chapter updates</string>
<string name="channel_app_updates">App updates</string>
<string name="channel_ext_updates">Extension updates</string>
<!-- S Pen actions -->