Browse Source

Migrate downloader service to WorkManager (#10190)

Ivan Iskandar 1 year ago
parent
commit
8ce8b60092

+ 0 - 1
app/build.gradle.kts

@@ -196,7 +196,6 @@ dependencies {
 
     // RxJava
     implementation(libs.rxjava)
-    implementation(libs.flowreactivenetwork)
 
     // Networking
     implementation(libs.bundles.okhttp)

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

@@ -21,6 +21,8 @@
 
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
     <uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
+    
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
 
     <!-- Remove permission from Firebase dependency -->
     <uses-permission android:name="com.google.android.gms.permission.AD_ID"
@@ -137,10 +139,6 @@
             android:name=".data.notification.NotificationReceiver"
             android:exported="false" />
 
-        <service
-            android:name=".data.download.DownloadService"
-            android:exported="false" />
-
         <service
             android:name=".extension.util.ExtensionInstallService"
             android:exported="false" />
@@ -154,6 +152,11 @@
                 android:value="true" />
         </service>
 
+        <service
+            android:name="androidx.work.impl.foreground.SystemForegroundService"
+            android:foregroundServiceType="dataSync"
+            tools:node="merge" />
+
         <provider
             android:name="androidx.core.content.FileProvider"
             android:authorities="${applicationId}.provider"

+ 121 - 0
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt

@@ -0,0 +1,121 @@
+package eu.kanade.tachiyomi.data.download
+
+import android.content.Context
+import android.content.pm.ServiceInfo
+import android.os.Build
+import androidx.lifecycle.asFlow
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingWorkPolicy
+import androidx.work.ForegroundInfo
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.util.system.isConnectedToWifi
+import eu.kanade.tachiyomi.util.system.isOnline
+import eu.kanade.tachiyomi.util.system.notificationBuilder
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import logcat.LogPriority
+import tachiyomi.core.util.system.logcat
+import tachiyomi.domain.download.service.DownloadPreferences
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+/**
+ * This worker is used to manage the downloader. The system can decide to stop the worker, in
+ * which case the downloader is also stopped. It's also stopped while there's no network available.
+ */
+class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
+
+    private val downloadManager: DownloadManager = Injekt.get()
+    private val downloadPreferences: DownloadPreferences = Injekt.get()
+
+    override suspend fun getForegroundInfo(): ForegroundInfo {
+        val notification = applicationContext.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
+            setContentTitle(applicationContext.getString(R.string.download_notifier_downloader_title))
+            setSmallIcon(android.R.drawable.stat_sys_download)
+        }.build()
+        return ForegroundInfo(
+            Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS,
+            notification,
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+            } else {
+                0
+            },
+        )
+    }
+
+    override suspend fun doWork(): Result {
+        try {
+            setForeground(getForegroundInfo())
+        } catch (e: IllegalStateException) {
+            logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
+        }
+
+        var networkCheck = checkConnectivity()
+        var active = networkCheck
+        downloadManager.downloaderStart()
+
+        // Keep the worker running when needed
+        while (active) {
+            delay(100)
+            networkCheck = checkConnectivity()
+            active = !isStopped && networkCheck && downloadManager.isRunning
+        }
+
+        return Result.success()
+    }
+
+    private fun checkConnectivity(): Boolean {
+        return with(applicationContext) {
+            if (isOnline()) {
+                val noWifi = downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()
+                if (noWifi) {
+                    downloadManager.downloaderStop(
+                        applicationContext.getString(R.string.download_notifier_text_only_wifi),
+                    )
+                }
+                !noWifi
+            } else {
+                downloadManager.downloaderStop(applicationContext.getString(R.string.download_notifier_no_network))
+                false
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "Downloader"
+
+        fun start(context: Context) {
+            val request = OneTimeWorkRequestBuilder<DownloadJob>()
+                .addTag(TAG)
+                .build()
+            WorkManager.getInstance(context)
+                .enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
+        }
+
+        fun stop(context: Context) {
+            WorkManager.getInstance(context)
+                .cancelUniqueWork(TAG)
+        }
+
+        fun isRunning(context: Context): Boolean {
+            return WorkManager.getInstance(context)
+                .getWorkInfosForUniqueWork(TAG)
+                .get()
+                .let { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
+        }
+
+        fun isRunningFlow(context: Context): Flow<Boolean> {
+            return WorkManager.getInstance(context)
+                .getWorkInfosForUniqueWorkLiveData(TAG)
+                .asFlow()
+                .map { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
+        }
+    }
+}

+ 8 - 5
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt

@@ -46,6 +46,9 @@ class DownloadManager(
      */
     private val downloader = Downloader(context, provider, cache)
 
+    val isRunning: Boolean
+        get() = downloader.isRunning
+
     /**
      * Queue to delay the deletion of a list of chapters until triggered.
      */
@@ -59,13 +62,13 @@ class DownloadManager(
     fun downloaderStop(reason: String? = null) = downloader.stop(reason)
 
     val isDownloaderRunning
-        get() = DownloadService.isRunning
+        get() = DownloadJob.isRunningFlow(context)
 
     /**
      * Tells the downloader to begin downloads.
      */
     fun startDownloads() {
-        DownloadService.start(context)
+        DownloadJob.start(context)
     }
 
     /**
@@ -104,10 +107,10 @@ class DownloadManager(
         queue.add(0, toAdd)
         reorderQueue(queue)
         if (!downloader.isRunning) {
-            if (DownloadService.isRunning(context)) {
+            if (DownloadJob.isRunning(context)) {
                 downloader.start()
             } else {
-                DownloadService.start(context)
+                DownloadJob.start(context)
             }
         }
     }
@@ -143,7 +146,7 @@ class DownloadManager(
             addAll(0, downloads)
             reorderQueue(this)
         }
-        if (!DownloadService.isRunning(context)) DownloadService.start(context)
+        if (!DownloadJob.isRunning(context)) DownloadJob.start(context)
     }
 
     /**

+ 0 - 151
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt

@@ -1,151 +0,0 @@
-package eu.kanade.tachiyomi.data.download
-
-import android.app.Notification
-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 dev.icerock.moko.resources.StringResource
-import eu.kanade.tachiyomi.data.notification.Notifications
-import eu.kanade.tachiyomi.util.system.acquireWakeLock
-import eu.kanade.tachiyomi.util.system.isConnectedToWifi
-import eu.kanade.tachiyomi.util.system.isOnline
-import eu.kanade.tachiyomi.util.system.isServiceRunning
-import eu.kanade.tachiyomi.util.system.notificationBuilder
-import eu.kanade.tachiyomi.util.system.toast
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import logcat.LogPriority
-import ru.beryukhov.reactivenetwork.ReactiveNetwork
-import tachiyomi.core.i18n.stringResource
-import tachiyomi.core.util.lang.withUIContext
-import tachiyomi.core.util.system.logcat
-import tachiyomi.domain.download.service.DownloadPreferences
-import tachiyomi.i18n.MR
-import uy.kohesive.injekt.injectLazy
-
-/**
- * This service is used to manage the downloader. The system can decide to stop the service, in
- * which case the downloader is also stopped. It's also stopped while there's no network available.
- * While the downloader is running, a wake lock will be held.
- */
-class DownloadService : Service() {
-
-    companion object {
-
-        private val _isRunning = MutableStateFlow(false)
-        val isRunning = _isRunning.asStateFlow()
-
-        /**
-         * Starts this service.
-         *
-         * @param context the application context.
-         */
-        fun start(context: Context) {
-            val intent = Intent(context, DownloadService::class.java)
-            ContextCompat.startForegroundService(context, intent)
-        }
-
-        /**
-         * Stops this service.
-         *
-         * @param context the application context.
-         */
-        fun stop(context: Context) {
-            context.stopService(Intent(context, DownloadService::class.java))
-        }
-
-        /**
-         * Returns the status of the service.
-         *
-         * @param context the application context.
-         * @return true if the service is running, false otherwise.
-         */
-        fun isRunning(context: Context): Boolean {
-            return context.isServiceRunning(DownloadService::class.java)
-        }
-    }
-
-    private val downloadManager: DownloadManager by injectLazy()
-    private val downloadPreferences: DownloadPreferences by injectLazy()
-
-    /**
-     * Wake lock to prevent the device to enter sleep mode.
-     */
-    private lateinit var wakeLock: PowerManager.WakeLock
-
-    private lateinit var scope: CoroutineScope
-
-    override fun onCreate() {
-        scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
-        startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
-        wakeLock = acquireWakeLock(javaClass.name)
-        _isRunning.value = true
-        listenNetworkChanges()
-    }
-
-    override fun onDestroy() {
-        scope.cancel()
-        _isRunning.value = false
-        downloadManager.downloaderStop()
-        if (wakeLock.isHeld) {
-            wakeLock.release()
-        }
-    }
-
-    // Not used
-    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
-        return START_NOT_STICKY
-    }
-
-    // Not used
-    override fun onBind(intent: Intent): IBinder? {
-        return null
-    }
-
-    private fun downloaderStop(string: StringResource) {
-        downloadManager.downloaderStop(stringResource(string))
-    }
-
-    private fun listenNetworkChanges() {
-        ReactiveNetwork()
-            .observeNetworkConnectivity(applicationContext)
-            .onEach {
-                withUIContext {
-                    if (isOnline()) {
-                        if (downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()) {
-                            downloaderStop(MR.strings.download_notifier_text_only_wifi)
-                        } else {
-                            val started = downloadManager.downloaderStart()
-                            if (!started) stopSelf()
-                        }
-                    } else {
-                        downloaderStop(MR.strings.download_notifier_no_network)
-                    }
-                }
-            }
-            .catch { error ->
-                withUIContext {
-                    logcat(LogPriority.ERROR, error)
-                    toast(MR.strings.download_queue_error)
-                    stopSelf()
-                }
-            }
-            .launchIn(scope)
-    }
-
-    private fun getPlaceholderNotification(): Notification {
-        return notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
-            setContentTitle(stringResource(MR.strings.download_notifier_downloader_title))
-        }.build()
-    }
-}

+ 2 - 5
app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

@@ -161,10 +161,7 @@ class Downloader(
 
         isPaused = false
 
-        // Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
-        if (DownloadService.isRunning.value) {
-            DownloadService.stop(context)
-        }
+        DownloadJob.stop(context)
     }
 
     /**
@@ -310,7 +307,7 @@ class Downloader(
                         )
                     }
                 }
-                DownloadService.start(context)
+                DownloadJob.start(context)
             }
         }
     }

+ 4 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt

@@ -11,12 +11,14 @@ import eu.kanade.tachiyomi.source.model.Page
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import uy.kohesive.injekt.Injekt
@@ -137,8 +139,8 @@ class DownloadQueueScreenModel(
         adapter = null
     }
 
-    val isDownloaderRunning
-        get() = downloadManager.isDownloaderRunning
+    val isDownloaderRunning = downloadManager.isDownloaderRunning
+        .stateIn(screenModelScope, SharingStarted.WhileSubscribed(5000), false)
 
     fun getDownloadStatusFlow() = downloadManager.statusFlow()
     fun getDownloadProgressFlow() = downloadManager.progressFlow()

+ 0 - 1
gradle/libs.versions.toml

@@ -15,7 +15,6 @@ android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1
 google-services-gradle = "com.google.gms:google-services:4.4.0"
 
 rxjava = "io.reactivex:rxjava:1.3.8"
-flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
 
 okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
 okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }