Download app update without WorkManager (#3707)
This commit is contained in:
@@ -65,11 +65,9 @@ object Notifications {
|
|||||||
const val ID_INCOGNITO_MODE = -701
|
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"
|
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 CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
const val ID_EXTENSION_INSTALLER = -402
|
const val ID_EXTENSION_INSTALLER = -402
|
||||||
@@ -84,6 +82,7 @@ object Notifications {
|
|||||||
"downloader_cache_renewal",
|
"downloader_cache_renewal",
|
||||||
"crash_logs_channel",
|
"crash_logs_channel",
|
||||||
"library_skipped_channel",
|
"library_skipped_channel",
|
||||||
|
"app_apk_update_channel",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,10 +156,6 @@ object Notifications {
|
|||||||
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||||
setName(context.stringResource(MR.strings.pref_incognito_mode))
|
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) {
|
buildNotificationChannel(CHANNEL_EXTENSIONS_UPDATE, IMPORTANCE_DEFAULT) {
|
||||||
setGroup(GROUP_APK_UPDATES)
|
setGroup(GROUP_APK_UPDATES)
|
||||||
setName(context.stringResource(MR.strings.channel_ext_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.CreationExtras
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
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.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.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import logcat.LogPriority
|
||||||
import mihon.core.viewmodel.StateViewModel
|
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.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class NewUpdateScreenModel(
|
class NewUpdateScreenModel(
|
||||||
changelogInfo: String,
|
changelogInfo: String,
|
||||||
private val downloadLink: String,
|
private val downloadLink: String,
|
||||||
private val context: Context = Injekt.get(),
|
private val context: Context = Injekt.get(),
|
||||||
|
private val network: NetworkHelper = Injekt.get(),
|
||||||
) : StateViewModel<NewUpdateScreenModel.State>(State(changelogInfo = changelogInfo)) {
|
) : StateViewModel<NewUpdateScreenModel.State>(State(changelogInfo = changelogInfo)) {
|
||||||
|
|
||||||
init {
|
private val apkFile: File
|
||||||
context.workManager.getWorkInfosByTagFlow(AppUpdateDownloadJob.TAG)
|
get() = File(context.externalCacheDir, "update.apk")
|
||||||
.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url != downloadLink) {
|
private var downloadJob: Job? = null
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startDownload() {
|
fun startDownload() {
|
||||||
mutableState.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) }
|
if (downloadJob?.isActive == true) return
|
||||||
AppUpdateDownloadJob.start(context, downloadLink)
|
|
||||||
|
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() {
|
fun installUpdate() {
|
||||||
val apkFile = AppUpdateDownloadJob.updateApk(context)
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(apkFile.getUriCompat(context), ExtensionInstaller.APK_MIME)
|
setDataAndType(apkFile.getUriCompat(context), ExtensionInstaller.APK_MIME)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
@@ -87,10 +88,6 @@ class NewUpdateScreenModel(
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
AppUpdateDownloadJob.stop(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val changelogInfo: String,
|
val changelogInfo: String,
|
||||||
|
|||||||
@@ -951,7 +951,6 @@
|
|||||||
<string name="update_check_no_new_updates">No new updates available</string>
|
<string name="update_check_no_new_updates">No new updates available</string>
|
||||||
|
|
||||||
<!--UpdateCheck Notifications-->
|
<!--UpdateCheck Notifications-->
|
||||||
<string name="update_check_notification_download_in_progress">Downloading…</string>
|
|
||||||
<string name="update_check_notification_update_available">New version available!</string>
|
<string name="update_check_notification_update_available">New version available!</string>
|
||||||
|
|
||||||
<!-- Information Text -->
|
<!-- Information Text -->
|
||||||
@@ -990,7 +989,6 @@
|
|||||||
<string name="channel_complete">Complete</string>
|
<string name="channel_complete">Complete</string>
|
||||||
<string name="channel_errors">Errors</string>
|
<string name="channel_errors">Errors</string>
|
||||||
<string name="channel_new_chapters">Chapter updates</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>
|
<string name="channel_ext_updates">Extension updates</string>
|
||||||
|
|
||||||
<!-- S Pen actions -->
|
<!-- S Pen actions -->
|
||||||
|
|||||||
Reference in New Issue
Block a user