diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c3c2f288c..442a123f3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -145,10 +145,6 @@
android:name=".data.download.DownloadService"
android:exported="false" />
-
-
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
index 5110aaca6..566da8e9c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
@@ -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
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt
new file mode 100644
index 000000000..60d5e71ec
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt
@@ -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()
+ .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)
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt
index 728967654..0437103a3 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt
@@ -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,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateService.kt
deleted file mode 100644
index c1f973d2d..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateService.kt
+++ /dev/null
@@ -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)
- }
- }
- }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt
index f6ebdab64..eae88d838 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt
@@ -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,