mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Migrate downloader service to WorkManager (#10190)
This commit is contained in:
		@@ -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 }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user