Selaa lähdekoodia

Replace AppUpdateService with a WorkManager job

Fixes #7773

Co-authored-by: Jays2Kings <[email protected]>
arkon 1 vuosi sitten
vanhempi
commit
eed57b80be

+ 0 - 4
app/src/main/AndroidManifest.xml

@@ -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" />

+ 26 - 3
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
             }

+ 148 - 0
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<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)
+        }
+    }
+}

+ 7 - 7
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,

+ 0 - 195
app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateService.kt

@@ -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)
-            }
-        }
-    }
-}

+ 2 - 2
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,