Replace AppUpdateService with a WorkManager job

Fixes #7773

Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
This commit is contained in:
arkon 2023-10-27 15:37:59 -04:00
parent c46c39d4ae
commit eed57b80be
6 changed files with 183 additions and 211 deletions

View file

@ -145,10 +145,6 @@
android:name=".data.download.DownloadService"
android:exported="false" />
<service
android:name=".data.updater.AppUpdateService"
android:exported="false" />
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false" />

View file

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
@ -85,6 +85,8 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_CANCEL_RESTORE -> cancelRestore(context)
// Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
// Start downloading app update
ACTION_START_APP_UPDATE -> startDownloadAppUpdate(context, intent)
// Cancel downloading app update
ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context)
// Open reader activity
@ -209,8 +211,13 @@ class NotificationReceiver : BroadcastReceiver() {
LibraryUpdateJob.stop(context)
}
private fun startDownloadAppUpdate(context: Context, intent: Intent) {
val url = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL) ?: return
AppUpdateDownloadJob.start(context, url)
}
private fun cancelDownloadAppUpdate(context: Context) {
AppUpdateService.stop(context)
AppUpdateDownloadJob.stop(context)
}
/**
@ -268,6 +275,7 @@ class NotificationReceiver : BroadcastReceiver() {
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE"
private const val ACTION_CANCEL_APP_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_APP_UPDATE_DOWNLOAD"
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
@ -499,10 +507,25 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
/**
* Returns [PendingIntent] that starts the [AppUpdateDownloadJob] to download an app update.
*
* @param context context of application
* @return [PendingIntent]
*/
internal fun downloadAppUpdatePendingBroadcast(context: Context, url: String, title: String? = null): PendingIntent {
return Intent(context, NotificationReceiver::class.java).run {
action = ACTION_START_APP_UPDATE
putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL, url)
title?.let { putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE, it) }
PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}
/**
*
*/
internal fun cancelUpdateDownloadPendingBroadcast(context: Context): PendingIntent {
internal fun cancelDownloadAppUpdatePendingBroadcast(context: Context): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD
}

View file

@ -0,0 +1,148 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
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.R
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.await
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.File
import kotlin.coroutines.cancellation.CancellationException
class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val notifier = AppUpdateNotifier(context)
private val network: NetworkHelper by injectLazy()
override suspend fun doWork(): Result {
val url = inputData.getString(EXTRA_DOWNLOAD_URL)
val title = inputData.getString(EXTRA_DOWNLOAD_TITLE) ?: context.getString(R.string.app_name)
if (url.isNullOrEmpty()) {
return Result.failure()
}
try {
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
}
withIOContext {
downloadApk(title, url)
}
return Result.success()
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
Notifications.ID_APP_UPDATER,
notifier.onDownloadStarted().build(),
)
}
/**
* Called to start downloading apk of new update
*
* @param url url location of file
*/
private suspend fun downloadApk(title: String, url: String) {
// Show notification download starting.
notifier.onDownloadStarted(title)
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
notifier.onProgressChange(progress)
}
}
}
try {
// Download the new update.
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
// File where the apk will be saved.
val apkFile = File(context.externalCacheDir, "update.apk")
if (response.isSuccessful) {
response.body.source().saveTo(apkFile)
} else {
response.close()
throw Exception("Unsuccessful response")
}
notifier.cancel()
notifier.promptInstall(apkFile.getUriCompat(context))
} catch (e: Exception) {
val shouldCancel = e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
if (shouldCancel) {
notifier.cancel()
} else {
notifier.onDownloadError(url)
}
}
}
companion object {
private const val TAG = "AppUpdateDownload"
const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL"
const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE"
fun start(context: Context, url: String, title: String? = null) {
val constraints = Constraints(
requiredNetworkType = NetworkType.CONNECTED,
)
val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>()
.setConstraints(constraints)
.addTag(TAG)
.setInputData(
workDataOf(
EXTRA_DOWNLOAD_URL to url,
EXTRA_DOWNLOAD_TITLE to title,
),
)
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
}
fun stop(context: Context) {
context.workManager.cancelUniqueWork(TAG)
}
}
}

View file

@ -34,11 +34,11 @@ internal class AppUpdateNotifier(private val context: Context) {
@SuppressLint("LaunchActivityFromNotification")
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 = NotificationReceiver.downloadAppUpdatePendingBroadcast(
context,
release.getDownloadLink(),
release.version,
)
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@ -82,7 +82,7 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel),
NotificationReceiver.cancelUpdateDownloadPendingBroadcast(context),
NotificationReceiver.cancelDownloadAppUpdatePendingBroadcast(context),
)
}
notificationBuilder.show()
@ -164,7 +164,7 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_refresh_24dp,
context.getString(R.string.action_retry),
AppUpdateService.downloadApkPendingService(context, url),
NotificationReceiver.downloadAppUpdatePendingBroadcast(context, url),
)
addAction(
R.drawable.ic_close_24dp,

View file

@ -1,195 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
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.await
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
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.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 uy.kohesive.injekt.injectLazy
import java.io.File
class AppUpdateService : Service() {
private val network: NetworkHelper by injectLazy()
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: AppUpdateNotifier
private val job = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
override fun onCreate() {
notifier = AppUpdateNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_APP_UPDATER, notifier.onDownloadStarted().build())
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) 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)
serviceScope.launch {
downloadApk(title, url)
}
job.invokeOnCompletion { stopSelf(startId) }
return START_NOT_STICKY
}
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
destroyJob()
}
private fun destroyJob() {
serviceScope.cancel()
job.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* Called to start downloading apk of new update
*
* @param url url location of file
*/
private suspend fun downloadApk(title: String, url: String) {
// Show notification download starting.
notifier.onDownloadStarted(title)
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
notifier.onProgressChange(progress)
}
}
}
try {
// Download the new update.
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
.await()
// File where the apk will be saved.
val apkFile = File(externalCacheDir, "update.apk")
if (response.isSuccessful) {
response.body.source().saveTo(apkFile)
} else {
response.close()
throw Exception("Unsuccessful response")
}
notifier.cancel()
notifier.promptInstall(apkFile.getUriCompat(this))
} catch (e: Exception) {
val shouldCancel = e is CancellationException ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
if (shouldCancel) {
notifier.cancel()
} else {
notifier.onDownloadError(url)
}
}
}
companion object {
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(AppUpdateService::class.java)
/**
* Downloads a new update and let the user install the new version from a notification.
*
* @param context the application context.
* @param url the url to the new update.
*/
fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
if (isRunning(context)) return
Intent(context, AppUpdateService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
ContextCompat.startForegroundService(context, this)
}
}
/**
* Stops the service.
*
* @param context the application context
*/
fun stop(context: Context) {
context.stopService(Intent(context, AppUpdateService::class.java))
}
/**
* Returns [PendingIntent] that starts a service which downloads the apk specified in url.
*
* @param url the url to the new update.
* @return [PendingIntent]
*/
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
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)
}
}
}
}

View file

@ -7,7 +7,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.NewUpdateScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
import eu.kanade.tachiyomi.util.system.openInBrowser
class NewUpdateScreen(
@ -31,7 +31,7 @@ class NewUpdateScreen(
onOpenInBrowser = { context.openInBrowser(releaseLink) },
onRejectUpdate = navigator::pop,
onAcceptUpdate = {
AppUpdateService.start(
AppUpdateDownloadJob.start(
context = context,
url = downloadLink,
title = versionName,