Migrate downloader service to WorkManager (#10190)

This commit is contained in:
Ivan Iskandar 2023-11-30 04:34:07 +07:00 committed by GitHub
parent 8ff2c01bf2
commit 8ce8b60092
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 169 deletions

View file

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

View file

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

View file

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

View file

@ -46,6 +46,9 @@ class DownloadManager(
*/ */
private val downloader = Downloader(context, provider, cache) 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. * 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) fun downloaderStop(reason: String? = null) = downloader.stop(reason)
val isDownloaderRunning val isDownloaderRunning
get() = DownloadService.isRunning get() = DownloadJob.isRunningFlow(context)
/** /**
* Tells the downloader to begin downloads. * Tells the downloader to begin downloads.
*/ */
fun startDownloads() { fun startDownloads() {
DownloadService.start(context) DownloadJob.start(context)
} }
/** /**
@ -104,10 +107,10 @@ class DownloadManager(
queue.add(0, toAdd) queue.add(0, toAdd)
reorderQueue(queue) reorderQueue(queue)
if (!downloader.isRunning) { if (!downloader.isRunning) {
if (DownloadService.isRunning(context)) { if (DownloadJob.isRunning(context)) {
downloader.start() downloader.start()
} else { } else {
DownloadService.start(context) DownloadJob.start(context)
} }
} }
} }
@ -143,7 +146,7 @@ class DownloadManager(
addAll(0, downloads) addAll(0, downloads)
reorderQueue(this) reorderQueue(this)
} }
if (!DownloadService.isRunning(context)) DownloadService.start(context) if (!DownloadJob.isRunning(context)) DownloadJob.start(context)
} }
/** /**

View file

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

View file

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

View file

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

View file

@ -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" google-services-gradle = "com.google.gms:google-services:4.4.0"
rxjava = "io.reactivex:rxjava:1.3.8" 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-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }