Download app update without WorkManager (#3707)
This commit is contained in:
@@ -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() {
|
||||
mutableState.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) }
|
||||
AppUpdateDownloadJob.start(context, downloadLink)
|
||||
if (downloadJob?.isActive == true) return
|
||||
|
||||
downloadJob = viewModelScope.launch {
|
||||
mutableState.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) }
|
||||
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 -->
|
||||
|
||||
Reference in New Issue
Block a user