diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b2c23342e..562ee34cc 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,7 +22,7 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
- versionCode = 95
+ versionCode = 96
versionName = "0.14.4"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9c211bffa..010ee07f0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -201,10 +201,6 @@
android:resource="@xml/updates_grid_glance_widget_info" />
-
-
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
index 1ddc04c91..547687c04 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
@@ -31,7 +31,7 @@ import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.network.NetworkHelper
@@ -307,13 +307,13 @@ object SettingsAdvancedScreen : SearchableSettings {
preferenceItems = listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_refresh_library_covers),
- onClick = { LibraryUpdateService.start(context, target = LibraryUpdateService.Target.COVERS) },
+ onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.COVERS) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_refresh_library_tracking),
subtitle = stringResource(R.string.pref_refresh_library_tracking_summary),
enabled = trackManager.hasLoggedServices(),
- onClick = { LibraryUpdateService.start(context, target = LibraryUpdateService.Target.TRACKING) },
+ onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.TRACKING) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_reset_viewer_flags),
diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
index cd8f088f1..20299f691 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
@@ -348,6 +348,10 @@ object Migrations {
}
}
}
+ if (oldVersion < 95) {
+ LibraryUpdateJob.cancelAllWorks(context)
+ LibraryUpdateJob.setupTask(context)
+ }
return true
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
index 51759c3d1..cd81e1877 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
@@ -1,44 +1,551 @@
package eu.kanade.tachiyomi.data.library
import android.content.Context
+import androidx.work.BackoffPolicy
import androidx.work.Constraints
+import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.ForegroundInfo
import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkInfo
import androidx.work.WorkManager
-import androidx.work.Worker
+import androidx.work.WorkQuery
import androidx.work.WorkerParameters
+import androidx.work.workDataOf
+import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
+import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
+import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
+import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.library.service.LibraryPreferences
+import eu.kanade.domain.manga.interactor.GetLibraryManga
+import eu.kanade.domain.manga.interactor.GetManga
+import eu.kanade.domain.manga.interactor.UpdateManga
+import eu.kanade.domain.manga.model.copyFrom
+import eu.kanade.domain.manga.model.toSManga
+import eu.kanade.domain.track.interactor.GetTracks
+import eu.kanade.domain.track.interactor.InsertTrack
+import eu.kanade.domain.track.model.toDbTrack
+import eu.kanade.domain.track.model.toDomainTrack
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.cache.CoverCache
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.data.download.DownloadService
+import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.DEVICE_BATTERY_NOT_LOW
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
import eu.kanade.tachiyomi.data.preference.DEVICE_NETWORK_NOT_METERED
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
+import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
+import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
+import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
+import eu.kanade.tachiyomi.data.track.EnhancedTrackService
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.UnmeteredSource
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.model.UpdateStrategy
+import eu.kanade.tachiyomi.util.prepUpdateCover
+import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
+import eu.kanade.tachiyomi.util.storage.getUriCompat
+import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.supervisorScope
+import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withPermit
+import kotlinx.coroutines.withContext
+import logcat.LogPriority
+import tachiyomi.core.preference.getAndSet
+import tachiyomi.core.util.lang.withIOContext
+import tachiyomi.core.util.system.logcat
+import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.category.model.Category
+import tachiyomi.domain.chapter.model.Chapter
+import tachiyomi.domain.chapter.model.NoChaptersException
+import tachiyomi.domain.library.model.LibraryManga
+import tachiyomi.domain.manga.model.Manga
+import tachiyomi.domain.manga.model.toMangaUpdate
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
+import java.io.File
+import java.util.Date
+import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
- Worker(context, workerParams) {
+ CoroutineWorker(context, workerParams) {
- override fun doWork(): Result {
+ private val sourceManager: SourceManager = Injekt.get()
+ private val downloadPreferences: DownloadPreferences = Injekt.get()
+ private val libraryPreferences: LibraryPreferences = Injekt.get()
+ private val downloadManager: DownloadManager = Injekt.get()
+ private val trackManager: TrackManager = Injekt.get()
+ private val coverCache: CoverCache = Injekt.get()
+ private val getLibraryManga: GetLibraryManga = Injekt.get()
+ private val getManga: GetManga = Injekt.get()
+ private val updateManga: UpdateManga = Injekt.get()
+ private val getChapterByMangaId: GetChapterByMangaId = Injekt.get()
+ private val getCategories: GetCategories = Injekt.get()
+ private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
+ private val getTracks: GetTracks = Injekt.get()
+ private val insertTrack: InsertTrack = Injekt.get()
+ private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
+
+ private val notifier = LibraryUpdateNotifier(context)
+
+ private var mangaToUpdate: List = mutableListOf()
+
+ override suspend fun doWork(): Result {
val preferences = Injekt.get()
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.failure()
}
- return if (LibraryUpdateService.start(context)) {
- Result.success()
- } else {
- Result.failure()
+ if (tags.contains(WORK_NAME_AUTO)) {
+ // Find a running manual worker. If exists, try again later
+ val otherRunningWorker = withContext(Dispatchers.IO) {
+ WorkManager.getInstance(context)
+ .getWorkInfosByTag(WORK_NAME_MANUAL)
+ .get()
+ .find { it.state == WorkInfo.State.RUNNING }
+ }
+ if (otherRunningWorker != null) {
+ return Result.retry()
+ }
}
+
+ try {
+ setForeground(getForegroundInfo())
+ } catch (e: IllegalStateException) {
+ logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
+ }
+
+ val target = inputData.getString(KEY_TARGET)?.let { Target.valueOf(it) } ?: Target.CHAPTERS
+
+ // If this is a chapter update; set the last update time to now
+ if (target == Target.CHAPTERS) {
+ libraryPreferences.libraryUpdateLastTimestamp().set(Date().time)
+ }
+
+ val categoryId = inputData.getLong(KEY_CATEGORY, -1L)
+ addMangaToQueue(categoryId)
+
+ return withIOContext {
+ try {
+ when (target) {
+ Target.CHAPTERS -> updateChapterList()
+ Target.COVERS -> updateCovers()
+ Target.TRACKING -> updateTrackings()
+ }
+ Result.success()
+ } catch (e: Exception) {
+ if (e is CancellationException) {
+ // Assume success although cancelled
+ Result.success()
+ } else {
+ logcat(LogPriority.ERROR, e)
+ Result.failure()
+ }
+ } finally {
+ notifier.cancelProgressNotification()
+ }
+ }
+ }
+
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val notifier = LibraryUpdateNotifier(context)
+ return ForegroundInfo(Notifications.ID_LIBRARY_PROGRESS, notifier.progressNotificationBuilder.build())
+ }
+
+ /**
+ * Adds list of manga to be updated.
+ *
+ * @param categoryId the ID of the category to update, or -1 if no category specified.
+ */
+ private fun addMangaToQueue(categoryId: Long) {
+ val libraryManga = runBlocking { getLibraryManga.await() }
+
+ val listToUpdate = if (categoryId != -1L) {
+ libraryManga.filter { it.category == categoryId }
+ } else {
+ val categoriesToUpdate = libraryPreferences.libraryUpdateCategories().get().map { it.toLong() }
+ val includedManga = if (categoriesToUpdate.isNotEmpty()) {
+ libraryManga.filter { it.category in categoriesToUpdate }
+ } else {
+ libraryManga
+ }
+
+ val categoriesToExclude = libraryPreferences.libraryUpdateCategoriesExclude().get().map { it.toLong() }
+ val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) {
+ libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id }
+ } else {
+ emptyList()
+ }
+
+ includedManga
+ .filterNot { it.manga.id in excludedMangaIds }
+ .distinctBy { it.manga.id }
+ }
+
+ mangaToUpdate = listToUpdate
+ .sortedBy { it.manga.title }
+
+ // Warn when excessively checking a single source
+ val maxUpdatesFromSource = mangaToUpdate
+ .groupBy { it.manga.source }
+ .filterKeys { sourceManager.get(it) !is UnmeteredSource }
+ .maxOfOrNull { it.value.size } ?: 0
+ if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
+ notifier.showQueueSizeWarningNotification()
+ }
+ }
+
+ /**
+ * Method that updates manga in [mangaToUpdate]. It's called in a background thread, so it's safe
+ * to do heavy operations or network calls here.
+ * For each manga it calls [updateManga] and updates the notification showing the current
+ * progress.
+ *
+ * @return an observable delivering the progress of each update.
+ */
+ private suspend fun updateChapterList() {
+ val semaphore = Semaphore(5)
+ val progressCount = AtomicInteger(0)
+ val currentlyUpdatingManga = CopyOnWriteArrayList()
+ val newUpdates = CopyOnWriteArrayList>>()
+ val skippedUpdates = CopyOnWriteArrayList>()
+ val failedUpdates = CopyOnWriteArrayList>()
+ val hasDownloads = AtomicBoolean(false)
+ val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
+ val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
+
+ coroutineScope {
+ mangaToUpdate.groupBy { it.manga.source }.values
+ .map { mangaInSource ->
+ async {
+ semaphore.withPermit {
+ mangaInSource.forEach { libraryManga ->
+ val manga = libraryManga.manga
+ ensureActive()
+
+ // Don't continue to update if manga is not in library
+ if (getManga.await(manga.id)?.favorite != true) {
+ return@forEach
+ }
+
+ withUpdateNotification(
+ currentlyUpdatingManga,
+ progressCount,
+ manga,
+ ) {
+ when {
+ MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
+ skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
+
+ MANGA_HAS_UNREAD in restrictions && libraryManga.unreadCount != 0L ->
+ skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_caught_up))
+
+ MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
+ skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
+
+ manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
+ skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
+
+ else -> {
+ try {
+ val newChapters = updateManga(manga)
+ .sortedByDescending { it.sourceOrder }
+
+ if (newChapters.isNotEmpty()) {
+ val categoryIds = getCategories.await(manga.id).map { it.id }
+ if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) {
+ downloadChapters(manga, newChapters)
+ hasDownloads.set(true)
+ }
+
+ libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
+
+ // Convert to the manga that contains new chapters
+ newUpdates.add(manga to newChapters.toTypedArray())
+ }
+ } catch (e: Throwable) {
+ val errorMessage = when (e) {
+ is NoChaptersException -> context.getString(R.string.no_chapters_error)
+ // failedUpdates will already have the source, don't need to copy it into the message
+ is SourceManager.SourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error)
+ else -> e.message
+ }
+ failedUpdates.add(manga to errorMessage)
+ }
+ }
+ }
+
+ if (libraryPreferences.autoUpdateTrackers().get()) {
+ updateTrackings(manga, loggedServices)
+ }
+ }
+ }
+ }
+ }
+ }
+ .awaitAll()
+ }
+
+ notifier.cancelProgressNotification()
+
+ if (newUpdates.isNotEmpty()) {
+ notifier.showUpdateNotifications(newUpdates)
+ if (hasDownloads.get()) {
+ DownloadService.start(context)
+ }
+ }
+
+ if (failedUpdates.isNotEmpty()) {
+ val errorFile = writeErrorFile(failedUpdates)
+ notifier.showUpdateErrorNotification(
+ failedUpdates.size,
+ errorFile.getUriCompat(context),
+ )
+ }
+ if (skippedUpdates.isNotEmpty()) {
+ notifier.showUpdateSkippedNotification(skippedUpdates.size)
+ }
+ }
+
+ private fun downloadChapters(manga: Manga, chapters: List) {
+ // We don't want to start downloading while the library is updating, because websites
+ // may don't like it and they could ban the user.
+ downloadManager.downloadChapters(manga, chapters, false)
+ }
+
+ /**
+ * Updates the chapters for the given manga and adds them to the database.
+ *
+ * @param manga the manga to update.
+ * @return a pair of the inserted and removed chapters.
+ */
+ private suspend fun updateManga(manga: Manga): List {
+ val source = sourceManager.getOrStub(manga.source)
+
+ // Update manga metadata if needed
+ if (libraryPreferences.autoUpdateMetadata().get()) {
+ val networkManga = source.getMangaDetails(manga.toSManga())
+ updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache)
+ }
+
+ val chapters = source.getChapterList(manga.toSManga())
+
+ // Get manga from database to account for if it was removed during the update and
+ // to get latest data so it doesn't get overwritten later on
+ val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
+
+ return syncChaptersWithSource.await(chapters, dbManga, source)
+ }
+
+ private suspend fun updateCovers() {
+ val semaphore = Semaphore(5)
+ val progressCount = AtomicInteger(0)
+ val currentlyUpdatingManga = CopyOnWriteArrayList()
+
+ coroutineScope {
+ mangaToUpdate.groupBy { it.manga.source }
+ .values
+ .map { mangaInSource ->
+ async {
+ semaphore.withPermit {
+ mangaInSource.forEach { libraryManga ->
+ val manga = libraryManga.manga
+ ensureActive()
+
+ withUpdateNotification(
+ currentlyUpdatingManga,
+ progressCount,
+ manga,
+ ) {
+ val source = sourceManager.get(manga.source) ?: return@withUpdateNotification
+ try {
+ val networkManga = source.getMangaDetails(manga.toSManga())
+ val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true)
+ .copyFrom(networkManga)
+ try {
+ updateManga.await(updatedManga.toMangaUpdate())
+ } catch (e: Exception) {
+ logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
+ }
+ } catch (e: Throwable) {
+ // Ignore errors and continue
+ logcat(LogPriority.ERROR, e)
+ }
+ }
+ }
+ }
+ }
+ }
+ .awaitAll()
+ }
+
+ notifier.cancelProgressNotification()
+ }
+
+ /**
+ * Method that updates the metadata of the connected tracking services. It's called in a
+ * background thread, so it's safe to do heavy operations or network calls here.
+ */
+ private suspend fun updateTrackings() {
+ coroutineScope {
+ var progressCount = 0
+ val loggedServices = trackManager.services.filter { it.isLogged }
+
+ mangaToUpdate.forEach { libraryManga ->
+ val manga = libraryManga.manga
+
+ ensureActive()
+
+ notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
+
+ // Update the tracking details.
+ updateTrackings(manga, loggedServices)
+ }
+
+ notifier.cancelProgressNotification()
+ }
+ }
+
+ private suspend fun updateTrackings(manga: Manga, loggedServices: List) {
+ getTracks.await(manga.id)
+ .map { track ->
+ supervisorScope {
+ async {
+ val service = trackManager.getService(track.syncId)
+ if (service != null && service in loggedServices) {
+ try {
+ val updatedTrack = service.refresh(track.toDbTrack())
+ insertTrack.await(updatedTrack.toDomainTrack()!!)
+
+ if (service is EnhancedTrackService) {
+ val chapters = getChapterByMangaId.await(manga.id)
+ syncChaptersWithTrackServiceTwoWay.await(chapters, track, service)
+ }
+ } catch (e: Throwable) {
+ // Ignore errors and continue
+ logcat(LogPriority.ERROR, e)
+ }
+ }
+ }
+ }
+ }
+ .awaitAll()
+ }
+
+ private suspend fun withUpdateNotification(
+ updatingManga: CopyOnWriteArrayList,
+ completed: AtomicInteger,
+ manga: Manga,
+ block: suspend () -> Unit,
+ ) {
+ coroutineScope {
+ ensureActive()
+
+ updatingManga.add(manga)
+ notifier.showProgressNotification(
+ updatingManga,
+ completed.get(),
+ mangaToUpdate.size,
+ )
+
+ block()
+
+ ensureActive()
+
+ updatingManga.remove(manga)
+ completed.getAndIncrement()
+ notifier.showProgressNotification(
+ updatingManga,
+ completed.get(),
+ mangaToUpdate.size,
+ )
+ }
+ }
+
+ /**
+ * Writes basic file of update errors to cache dir.
+ */
+ private fun writeErrorFile(errors: List>): File {
+ try {
+ if (errors.isNotEmpty()) {
+ val file = context.createFileInCacheDir("tachiyomi_update_errors.txt")
+ file.bufferedWriter().use { out ->
+ out.write(context.getString(R.string.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
+ // Error file format:
+ // ! Error
+ // # Source
+ // - Manga
+ errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
+ out.write("\n! ${error}\n")
+ mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
+ val source = sourceManager.getOrStub(srcId)
+ out.write(" # $source\n")
+ mangas.forEach {
+ out.write(" - ${it.title}\n")
+ }
+ }
+ }
+ }
+ return file
+ }
+ } catch (_: Exception) {}
+ return File("")
+ }
+
+ /**
+ * Defines what should be updated within a service execution.
+ */
+ enum class Target {
+ CHAPTERS, // Manga chapters
+ COVERS, // Manga covers
+ TRACKING, // Tracking metadata
}
companion object {
private const val TAG = "LibraryUpdate"
+ private const val WORK_NAME_AUTO = "LibraryUpdate-auto"
+ private const val WORK_NAME_MANUAL = "LibraryUpdate-manual"
- fun setupTask(context: Context, prefInterval: Int? = null) {
+ private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting"
+
+ private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
+
+ /**
+ * Key for category to update.
+ */
+ private const val KEY_CATEGORY = "category"
+
+ /**
+ * Key that defines what should be updated.
+ */
+ private const val KEY_TARGET = "target"
+
+ fun cancelAllWorks(context: Context) {
+ WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
+ }
+
+ fun setupTask(
+ context: Context,
+ prefInterval: Int? = null,
+ ) {
val preferences = Injekt.get()
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
if (interval > 0) {
@@ -56,15 +563,58 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
TimeUnit.MINUTES,
)
.addTag(TAG)
+ .addTag(WORK_NAME_AUTO)
.setConstraints(constraints)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
.build()
- // Re-enqueue work because of common support suggestion to change
- // the settings on the desired time to schedule it at that time
- WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, request)
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_NAME_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request)
} else {
- WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
+ WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME_AUTO)
}
}
+
+ fun startNow(
+ context: Context,
+ category: Category? = null,
+ target: Target = Target.CHAPTERS,
+ ): Boolean {
+ val wm = WorkManager.getInstance(context)
+ val infos = wm.getWorkInfosByTag(TAG).get()
+ if (infos.find { it.state == WorkInfo.State.RUNNING } != null) {
+ // Already running either as a scheduled or manual job
+ return false
+ }
+
+ val inputData = workDataOf(
+ KEY_CATEGORY to category?.id,
+ KEY_TARGET to target.name,
+ )
+ val request = OneTimeWorkRequestBuilder()
+ .addTag(TAG)
+ .addTag(WORK_NAME_MANUAL)
+ .setInputData(inputData)
+ .build()
+ wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
+
+ return true
+ }
+
+ fun stop(context: Context) {
+ val wm = WorkManager.getInstance(context)
+ val workQuery = WorkQuery.Builder.fromTags(listOf(TAG))
+ .addStates(listOf(WorkInfo.State.RUNNING))
+ .build()
+ wm.getWorkInfos(workQuery).get()
+ // Should only return one work but just in case
+ .forEach {
+ wm.cancelWorkById(it.id)
+
+ // Re-enqueue cancelled scheduled work
+ if (it.tags.contains(WORK_NAME_AUTO)) {
+ setupTask(context)
+ }
+ }
+ }
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
deleted file mode 100644
index 021bfdddb..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
+++ /dev/null
@@ -1,607 +0,0 @@
-package eu.kanade.tachiyomi.data.library
-
-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.domain.chapter.interactor.GetChapterByMangaId
-import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
-import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
-import eu.kanade.domain.download.service.DownloadPreferences
-import eu.kanade.domain.library.service.LibraryPreferences
-import eu.kanade.domain.manga.interactor.GetLibraryManga
-import eu.kanade.domain.manga.interactor.GetManga
-import eu.kanade.domain.manga.interactor.UpdateManga
-import eu.kanade.domain.manga.model.copyFrom
-import eu.kanade.domain.manga.model.toSManga
-import eu.kanade.domain.track.interactor.GetTracks
-import eu.kanade.domain.track.interactor.InsertTrack
-import eu.kanade.domain.track.model.toDbTrack
-import eu.kanade.domain.track.model.toDomainTrack
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.data.download.DownloadService
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
-import eu.kanade.tachiyomi.data.notification.Notifications
-import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
-import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
-import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
-import eu.kanade.tachiyomi.data.track.EnhancedTrackService
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.source.UnmeteredSource
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.model.UpdateStrategy
-import eu.kanade.tachiyomi.util.prepUpdateCover
-import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
-import eu.kanade.tachiyomi.util.storage.getUriCompat
-import eu.kanade.tachiyomi.util.system.acquireWakeLock
-import eu.kanade.tachiyomi.util.system.createFileInCacheDir
-import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
-import eu.kanade.tachiyomi.util.system.isServiceRunning
-import kotlinx.coroutines.CoroutineExceptionHandler
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.supervisorScope
-import kotlinx.coroutines.sync.Semaphore
-import kotlinx.coroutines.sync.withPermit
-import logcat.LogPriority
-import tachiyomi.core.preference.getAndSet
-import tachiyomi.core.util.lang.withIOContext
-import tachiyomi.core.util.system.logcat
-import tachiyomi.domain.category.interactor.GetCategories
-import tachiyomi.domain.category.model.Category
-import tachiyomi.domain.chapter.model.Chapter
-import tachiyomi.domain.chapter.model.NoChaptersException
-import tachiyomi.domain.library.model.LibraryManga
-import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.manga.model.toMangaUpdate
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.io.File
-import java.util.Date
-import java.util.concurrent.CopyOnWriteArrayList
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicInteger
-
-/**
- * This class will take care of updating the chapters of the manga from the library. It can be
- * started calling the [start] method. If it's already running, it won't do anything.
- * While the library is updating, a [PowerManager.WakeLock] will be held until the update is
- * completed, preventing the device from going to sleep mode. A notification will display the
- * progress of the update, and if case of an unexpected error, this service will be silently
- * destroyed.
- */
-class LibraryUpdateService(
- val sourceManager: SourceManager = Injekt.get(),
- val downloadPreferences: DownloadPreferences = Injekt.get(),
- val libraryPreferences: LibraryPreferences = Injekt.get(),
- val downloadManager: DownloadManager = Injekt.get(),
- val trackManager: TrackManager = Injekt.get(),
- val coverCache: CoverCache = Injekt.get(),
- private val getLibraryManga: GetLibraryManga = Injekt.get(),
- private val getManga: GetManga = Injekt.get(),
- private val updateManga: UpdateManga = Injekt.get(),
- private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
- private val getCategories: GetCategories = Injekt.get(),
- private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
- private val getTracks: GetTracks = Injekt.get(),
- private val insertTrack: InsertTrack = Injekt.get(),
- private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
-) : Service() {
-
- private lateinit var wakeLock: PowerManager.WakeLock
- private lateinit var notifier: LibraryUpdateNotifier
- private var scope: CoroutineScope? = null
-
- private var mangaToUpdate: List = mutableListOf()
- private var updateJob: Job? = null
-
- /**
- * Defines what should be updated within a service execution.
- */
- enum class Target {
- CHAPTERS, // Manga chapters
- COVERS, // Manga covers
- TRACKING, // Tracking metadata
- }
-
- companion object {
-
- private var instance: LibraryUpdateService? = null
-
- /**
- * Key for category to update.
- */
- const val KEY_CATEGORY = "category"
-
- /**
- * Key that defines what should be updated.
- */
- const val KEY_TARGET = "target"
-
- /**
- * 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(LibraryUpdateService::class.java)
- }
-
- /**
- * Starts the service. It will be started only if there isn't another instance already
- * running.
- *
- * @param context the application context.
- * @param category a specific category to update, or null for global update.
- * @param target defines what should be updated.
- * @return true if service newly started, false otherwise
- */
- fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
- if (isRunning(context)) return false
-
- val intent = Intent(context, LibraryUpdateService::class.java).apply {
- putExtra(KEY_TARGET, target)
- category?.let { putExtra(KEY_CATEGORY, it.id) }
- }
- ContextCompat.startForegroundService(context, intent)
-
- return true
- }
-
- /**
- * Stops the service.
- *
- * @param context the application context.
- */
- fun stop(context: Context) {
- context.stopService(Intent(context, LibraryUpdateService::class.java))
- }
- }
-
- /**
- * Method called when the service is created. It injects dagger dependencies and acquire
- * the wake lock.
- */
- override fun onCreate() {
- notifier = LibraryUpdateNotifier(this)
- wakeLock = acquireWakeLock(javaClass.name)
-
- startForeground(Notifications.ID_LIBRARY_PROGRESS, notifier.progressNotificationBuilder.build())
- }
-
- /**
- * Method called when the service is destroyed. It destroys subscriptions and releases the wake
- * lock.
- */
- override fun onDestroy() {
- updateJob?.cancel()
- scope?.cancel()
- if (wakeLock.isHeld) {
- wakeLock.release()
- }
- if (instance == this) {
- instance = null
- }
- }
-
- /**
- * This method needs to be implemented, but it's not used/needed.
- */
- override fun onBind(intent: Intent): IBinder? = null
-
- /**
- * Method called when the service receives an intent.
- *
- * @param intent the start intent from.
- * @param flags the flags of the command.
- * @param startId the start id of this command.
- * @return the start value of the command.
- */
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (intent == null) return START_NOT_STICKY
- val target = intent.getSerializableExtraCompat(KEY_TARGET)
- ?: return START_NOT_STICKY
-
- instance = this
-
- // Unsubscribe from any previous subscription if needed
- updateJob?.cancel()
- scope?.cancel()
-
- // If this is a chapter update; set the last update time to now
- if (target == Target.CHAPTERS) {
- libraryPreferences.libraryUpdateLastTimestamp().set(Date().time)
- }
-
- // Update favorite manga
- val categoryId = intent.getLongExtra(KEY_CATEGORY, -1L)
- addMangaToQueue(categoryId)
-
- // Destroy service when completed or in case of an error.
- val handler = CoroutineExceptionHandler { _, exception ->
- logcat(LogPriority.ERROR, exception)
- stopSelf(startId)
- }
- scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
- updateJob = scope?.launch(handler) {
- when (target) {
- Target.CHAPTERS -> updateChapterList()
- Target.COVERS -> updateCovers()
- Target.TRACKING -> updateTrackings()
- }
- }
- updateJob?.invokeOnCompletion { stopSelf(startId) }
-
- return START_REDELIVER_INTENT
- }
-
- private val isUpdateJobActive: Boolean
- get() = (updateJob?.isActive == true)
-
- /**
- * Adds list of manga to be updated.
- *
- * @param categoryId the ID of the category to update, or -1 if no category specified.
- */
- private fun addMangaToQueue(categoryId: Long) {
- val libraryManga = runBlocking { getLibraryManga.await() }
-
- val listToUpdate = if (categoryId != -1L) {
- libraryManga.filter { it.category == categoryId }
- } else {
- val categoriesToUpdate = libraryPreferences.libraryUpdateCategories().get().map { it.toLong() }
- val includedManga = if (categoriesToUpdate.isNotEmpty()) {
- libraryManga.filter { it.category in categoriesToUpdate }
- } else {
- libraryManga
- }
-
- val categoriesToExclude = libraryPreferences.libraryUpdateCategoriesExclude().get().map { it.toLong() }
- val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) {
- libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id }
- } else {
- emptyList()
- }
-
- includedManga
- .filterNot { it.manga.id in excludedMangaIds }
- .distinctBy { it.manga.id }
- }
-
- mangaToUpdate = listToUpdate
- .sortedBy { it.manga.title }
-
- // Warn when excessively checking a single source
- val maxUpdatesFromSource = mangaToUpdate
- .groupBy { it.manga.source }
- .filterKeys { sourceManager.get(it) !is UnmeteredSource }
- .maxOfOrNull { it.value.size } ?: 0
- if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
- notifier.showQueueSizeWarningNotification()
- }
- }
-
- /**
- * Method that updates manga in [mangaToUpdate]. It's called in a background thread, so it's safe
- * to do heavy operations or network calls here.
- * For each manga it calls [updateManga] and updates the notification showing the current
- * progress.
- *
- * @return an observable delivering the progress of each update.
- */
- private suspend fun updateChapterList() {
- val semaphore = Semaphore(5)
- val progressCount = AtomicInteger(0)
- val currentlyUpdatingManga = CopyOnWriteArrayList()
- val newUpdates = CopyOnWriteArrayList>>()
- val skippedUpdates = CopyOnWriteArrayList>()
- val failedUpdates = CopyOnWriteArrayList>()
- val hasDownloads = AtomicBoolean(false)
- val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
- val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
-
- withIOContext {
- mangaToUpdate.groupBy { it.manga.source }.values
- .map { mangaInSource ->
- async {
- semaphore.withPermit {
- mangaInSource.forEach { libraryManga ->
- val manga = libraryManga.manga
- if (!isUpdateJobActive) {
- notifier.cancelProgressNotification()
- return@async
- }
-
- // Don't continue to update if manga is not in library
- if (getManga.await(manga.id)?.favorite != true) {
- return@forEach
- }
-
- withUpdateNotification(
- currentlyUpdatingManga,
- progressCount,
- manga,
- ) {
- when {
- MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
- skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
-
- MANGA_HAS_UNREAD in restrictions && libraryManga.unreadCount != 0L ->
- skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
-
- MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
- skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
-
- manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
- skippedUpdates.add(manga to getString(R.string.skipped_reason_not_always_update))
-
- else -> {
- try {
- val newChapters = updateManga(manga)
- .sortedByDescending { it.sourceOrder }
-
- if (newChapters.isNotEmpty()) {
- val categoryIds = getCategories.await(manga.id).map { it.id }
- if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) {
- downloadChapters(manga, newChapters)
- hasDownloads.set(true)
- }
-
- libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
-
- // Convert to the manga that contains new chapters
- newUpdates.add(manga to newChapters.toTypedArray())
- }
- } catch (e: Throwable) {
- val errorMessage = when (e) {
- is NoChaptersException -> getString(R.string.no_chapters_error)
- // failedUpdates will already have the source, don't need to copy it into the message
- is SourceManager.SourceNotInstalledException -> getString(R.string.loader_not_implemented_error)
- else -> e.message
- }
- failedUpdates.add(manga to errorMessage)
- }
- }
- }
-
- if (libraryPreferences.autoUpdateTrackers().get()) {
- updateTrackings(manga, loggedServices)
- }
- }
- }
- }
- }
- }
- .awaitAll()
- }
-
- notifier.cancelProgressNotification()
-
- if (newUpdates.isNotEmpty()) {
- notifier.showUpdateNotifications(newUpdates)
- if (hasDownloads.get()) {
- DownloadService.start(this)
- }
- }
-
- if (failedUpdates.isNotEmpty()) {
- val errorFile = writeErrorFile(failedUpdates)
- notifier.showUpdateErrorNotification(
- failedUpdates.size,
- errorFile.getUriCompat(this),
- )
- }
- if (skippedUpdates.isNotEmpty()) {
- notifier.showUpdateSkippedNotification(skippedUpdates.size)
- }
- }
-
- private fun downloadChapters(manga: Manga, chapters: List) {
- // We don't want to start downloading while the library is updating, because websites
- // may don't like it and they could ban the user.
- downloadManager.downloadChapters(manga, chapters, false)
- }
-
- /**
- * Updates the chapters for the given manga and adds them to the database.
- *
- * @param manga the manga to update.
- * @return a pair of the inserted and removed chapters.
- */
- private suspend fun updateManga(manga: Manga): List {
- val source = sourceManager.getOrStub(manga.source)
-
- // Update manga metadata if needed
- if (libraryPreferences.autoUpdateMetadata().get()) {
- val networkManga = source.getMangaDetails(manga.toSManga())
- updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache)
- }
-
- val chapters = source.getChapterList(manga.toSManga())
-
- // Get manga from database to account for if it was removed during the update and
- // to get latest data so it doesn't get overwritten later on
- val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
-
- return syncChaptersWithSource.await(chapters, dbManga, source)
- }
-
- private suspend fun updateCovers() {
- val semaphore = Semaphore(5)
- val progressCount = AtomicInteger(0)
- val currentlyUpdatingManga = CopyOnWriteArrayList()
-
- withIOContext {
- mangaToUpdate.groupBy { it.manga.source }
- .values
- .map { mangaInSource ->
- async {
- semaphore.withPermit {
- mangaInSource.forEach { libraryManga ->
- val manga = libraryManga.manga
- if (!isUpdateJobActive) {
- notifier.cancelProgressNotification()
- return@async
- }
-
- withUpdateNotification(
- currentlyUpdatingManga,
- progressCount,
- manga,
- ) {
- val source = sourceManager.get(manga.source) ?: return@withUpdateNotification
- try {
- val networkManga = source.getMangaDetails(manga.toSManga())
- val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true)
- .copyFrom(networkManga)
- try {
- updateManga.await(updatedManga.toMangaUpdate())
- } catch (e: Exception) {
- logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
- }
- } catch (e: Throwable) {
- // Ignore errors and continue
- logcat(LogPriority.ERROR, e)
- }
- }
- }
- }
- }
- }
- .awaitAll()
- }
-
- notifier.cancelProgressNotification()
- }
-
- /**
- * Method that updates the metadata of the connected tracking services. It's called in a
- * background thread, so it's safe to do heavy operations or network calls here.
- */
- private suspend fun updateTrackings() {
- var progressCount = 0
- val loggedServices = trackManager.services.filter { it.isLogged }
-
- mangaToUpdate.forEach { libraryManga ->
- val manga = libraryManga.manga
- if (!isUpdateJobActive) {
- notifier.cancelProgressNotification()
- return
- }
-
- notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
-
- // Update the tracking details.
- updateTrackings(manga, loggedServices)
- }
-
- notifier.cancelProgressNotification()
- }
-
- private suspend fun updateTrackings(manga: Manga, loggedServices: List) {
- getTracks.await(manga.id)
- .map { track ->
- supervisorScope {
- async {
- val service = trackManager.getService(track.syncId)
- if (service != null && service in loggedServices) {
- try {
- val updatedTrack = service.refresh(track.toDbTrack())
- insertTrack.await(updatedTrack.toDomainTrack()!!)
-
- if (service is EnhancedTrackService) {
- val chapters = getChapterByMangaId.await(manga.id)
- syncChaptersWithTrackServiceTwoWay.await(chapters, track, service)
- }
- } catch (e: Throwable) {
- // Ignore errors and continue
- logcat(LogPriority.ERROR, e)
- }
- }
- }
- }
- }
- .awaitAll()
- }
-
- private suspend fun withUpdateNotification(
- updatingManga: CopyOnWriteArrayList,
- completed: AtomicInteger,
- manga: Manga,
- block: suspend () -> Unit,
- ) {
- if (!isUpdateJobActive) {
- notifier.cancelProgressNotification()
- return
- }
-
- updatingManga.add(manga)
- notifier.showProgressNotification(
- updatingManga,
- completed.get(),
- mangaToUpdate.size,
- )
-
- block()
-
- if (!isUpdateJobActive) {
- notifier.cancelProgressNotification()
- return
- }
-
- updatingManga.remove(manga)
- completed.getAndIncrement()
- notifier.showProgressNotification(
- updatingManga,
- completed.get(),
- mangaToUpdate.size,
- )
- }
-
- /**
- * Writes basic file of update errors to cache dir.
- */
- private fun writeErrorFile(errors: List>): File {
- try {
- if (errors.isNotEmpty()) {
- val file = createFileInCacheDir("tachiyomi_update_errors.txt")
- file.bufferedWriter().use { out ->
- out.write(getString(R.string.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
- // Error file format:
- // ! Error
- // # Source
- // - Manga
- errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
- out.write("\n! ${error}\n")
- mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
- val source = sourceManager.getOrStub(srcId)
- out.write(" # $source\n")
- mangas.forEach {
- out.write(" - ${it.title}\n")
- }
- }
- }
- }
- return file
- }
- } catch (_: Exception) {}
- return File("")
- }
-}
-
-private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
-private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
index d471938c1..046cc4898 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
@@ -16,7 +16,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity
@@ -91,7 +91,7 @@ class NotificationReceiver : BroadcastReceiver() {
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
// Cancel library update and dismiss notification
- ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
+ ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
// Cancel downloading app update
ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context)
// Open reader activity
@@ -221,11 +221,9 @@ class NotificationReceiver : BroadcastReceiver() {
* Method called when user wants to stop a library update
*
* @param context context of application
- * @param notificationId id of notification
*/
- private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
- LibraryUpdateService.stop(context)
- ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
+ private fun cancelLibraryUpdate(context: Context) {
+ LibraryUpdateJob.stop(context)
}
private fun cancelDownloadAppUpdate(context: Context) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
index 6aa817b9d..ca44167b7 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
@@ -41,7 +41,7 @@ import eu.kanade.presentation.library.components.LibraryToolbar
import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.home.HomeScreen
@@ -89,7 +89,7 @@ object LibraryTab : Tab {
val snackbarHostState = remember { SnackbarHostState() }
val onClickRefresh: (Category?) -> Boolean = {
- val started = LibraryUpdateService.start(context, it)
+ val started = LibraryUpdateJob.startNow(context, it)
scope.launch {
val msgRes = if (started) R.string.updating_category else R.string.update_already_running
snackbarHostState.showSnackbar(context.getString(msgRes))
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
index cd85c55d6..51d800d36 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt
@@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.lang.toRelativeString
@@ -130,7 +130,7 @@ class UpdatesScreenModel(
}
fun updateLibrary(): Boolean {
- val started = LibraryUpdateService.start(Injekt.get())
+ val started = LibraryUpdateJob.startNow(Injekt.get())
coroutineScope.launch {
_events.send(Event.LibraryUpdateTriggered(started))
}