mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-26 23:18:17 +03:00
feat(app): Migrate downloader service to WorkManager (#10190)
This commit is contained in:
parent
8acb3887b2
commit
1f7ae8e7bb
13 changed files with 280 additions and 338 deletions
|
@ -204,7 +204,6 @@ dependencies {
|
|||
|
||||
// RxJava
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.flowreactivenetwork)
|
||||
|
||||
// Networking
|
||||
implementation(libs.bundles.okhttp)
|
||||
|
|
|
@ -23,8 +23,9 @@
|
|||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
|
@ -201,11 +202,7 @@
|
|||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.download.manga.MangaDownloadService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.download.anime.AnimeDownloadService"
|
||||
android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
|
@ -224,6 +221,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"
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
package eu.kanade.tachiyomi.data.download.anime
|
||||
|
||||
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 AnimeDownloadJob(context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val downloadManager: AnimeDownloadManager = 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_EPISODE_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 = "AnimeDownloader"
|
||||
|
||||
fun start(context: Context) {
|
||||
val request = OneTimeWorkRequestBuilder<AnimeDownloadJob>()
|
||||
.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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,6 +51,8 @@ class AnimeDownloadManager(
|
|||
*/
|
||||
private val downloader = AnimeDownloader(context, provider, cache, sourceManager)
|
||||
|
||||
val isRunning: Boolean
|
||||
get() = downloader.isRunning
|
||||
/**
|
||||
* Queue to delay the deletion of a list of episodes until triggered.
|
||||
*/
|
||||
|
@ -64,13 +66,13 @@ class AnimeDownloadManager(
|
|||
fun downloaderStop(reason: String? = null) = downloader.stop(reason)
|
||||
|
||||
val isDownloaderRunning
|
||||
get() = AnimeDownloadService.isRunning
|
||||
get() = AnimeDownloadJob.isRunningFlow(context)
|
||||
|
||||
/**
|
||||
* Tells the downloader to begin downloads.
|
||||
*/
|
||||
fun startDownloads() {
|
||||
AnimeDownloadService.start(context)
|
||||
AnimeDownloadJob.start(context)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,10 +111,10 @@ class AnimeDownloadManager(
|
|||
queue.add(0, toAdd)
|
||||
reorderQueue(queue)
|
||||
if (!downloader.isRunning) {
|
||||
if (AnimeDownloadService.isRunning(context)) {
|
||||
if (AnimeDownloadJob.isRunning(context)) {
|
||||
downloader.start()
|
||||
} else {
|
||||
AnimeDownloadService.start(context)
|
||||
AnimeDownloadJob.start(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +157,7 @@ class AnimeDownloadManager(
|
|||
addAll(0, downloads)
|
||||
reorderQueue(this)
|
||||
}
|
||||
if (!AnimeDownloadService.isRunning(context)) AnimeDownloadService.start(context)
|
||||
if (!AnimeDownloadJob.isRunning(context)) AnimeDownloadJob.start(context)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.download.anime
|
||||
|
||||
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 AnimeDownloadService : 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, AnimeDownloadService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, AnimeDownloadService::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(AnimeDownloadService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val downloadManager: AnimeDownloadManager by injectLazy()
|
||||
private val downloadPreferences: DownloadPreferences by injectLazy()
|
||||
|
||||
/**
|
||||
* Wake lock to prevent the device to enter sleep mode.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
/**
|
||||
* Subscriptions to store while the service is running.
|
||||
*/
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
override fun onCreate() {
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
startForeground(Notifications.ID_DOWNLOAD_EPISODE_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()
|
||||
}
|
||||
}
|
|
@ -171,10 +171,7 @@ class AnimeDownloader(
|
|||
|
||||
isPaused = false
|
||||
|
||||
// Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
|
||||
if (AnimeDownloadService.isRunning.value) {
|
||||
AnimeDownloadService.stop(context)
|
||||
}
|
||||
AnimeDownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -349,7 +346,7 @@ class AnimeDownloader(
|
|||
)
|
||||
}
|
||||
}
|
||||
AnimeDownloadService.start(context)
|
||||
AnimeDownloadJob.start(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
package eu.kanade.tachiyomi.data.download.manga
|
||||
|
||||
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 MangaDownloadJob(context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val downloadManager: MangaDownloadManager = 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 = "MangaDownloader"
|
||||
|
||||
fun start(context: Context) {
|
||||
val request = OneTimeWorkRequestBuilder<MangaDownloadJob>()
|
||||
.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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,6 +52,9 @@ class MangaDownloadManager(
|
|||
*/
|
||||
private val downloader = MangaDownloader(context, provider, cache)
|
||||
|
||||
val isRunning: Boolean
|
||||
get() = downloader.isRunning
|
||||
|
||||
/**
|
||||
* Queue to delay the deletion of a list of chapters until triggered.
|
||||
*/
|
||||
|
@ -65,13 +68,13 @@ class MangaDownloadManager(
|
|||
fun downloaderStop(reason: String? = null) = downloader.stop(reason)
|
||||
|
||||
val isDownloaderRunning
|
||||
get() = MangaDownloadService.isRunning
|
||||
get() = MangaDownloadJob.isRunningFlow(context)
|
||||
|
||||
/**
|
||||
* Tells the downloader to begin downloads.
|
||||
*/
|
||||
fun startDownloads() {
|
||||
MangaDownloadService.start(context)
|
||||
MangaDownloadJob.start(context)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,10 +113,10 @@ class MangaDownloadManager(
|
|||
queue.add(0, toAdd)
|
||||
reorderQueue(queue)
|
||||
if (!downloader.isRunning) {
|
||||
if (MangaDownloadService.isRunning(context)) {
|
||||
if (MangaDownloadJob.isRunning(context)) {
|
||||
downloader.start()
|
||||
} else {
|
||||
MangaDownloadService.start(context)
|
||||
MangaDownloadJob.start(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +152,7 @@ class MangaDownloadManager(
|
|||
addAll(0, downloads)
|
||||
reorderQueue(this)
|
||||
}
|
||||
if (!MangaDownloadService.isRunning(context)) MangaDownloadService.start(context)
|
||||
if (!MangaDownloadJob.isRunning(context)) MangaDownloadJob.start(context)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.download.manga
|
||||
|
||||
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 MangaDownloadService : 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, MangaDownloadService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, MangaDownloadService::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(MangaDownloadService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val downloadManager: MangaDownloadManager 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()
|
||||
}
|
||||
}
|
|
@ -171,10 +171,7 @@ class MangaDownloader(
|
|||
|
||||
isPaused = false
|
||||
|
||||
// Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
|
||||
if (MangaDownloadService.isRunning.value) {
|
||||
MangaDownloadService.stop(context)
|
||||
}
|
||||
MangaDownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -325,7 +322,7 @@ class MangaDownloader(
|
|||
)
|
||||
}
|
||||
}
|
||||
MangaDownloadService.start(context)
|
||||
MangaDownloadJob.start(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.kanade.tachiyomi.ui.download.anime
|
||||
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import android.view.MenuItem
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
|
@ -131,8 +133,8 @@ class AnimeDownloadQueueScreenModel(
|
|||
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()
|
||||
|
|
|
@ -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 MangaDownloadQueueScreenModel(
|
|||
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()
|
||||
|
|
|
@ -22,7 +22,6 @@ desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
|
|||
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
||||
|
||||
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" }
|
||||
|
|
Loading…
Reference in a new issue