Remove update library option + Library updater now uses a queue system

Also reordered group notifaction so it always sends first
This commit is contained in:
Jay 2020-02-04 21:25:40 -08:00
parent 0aca015171
commit adc3a522cd
4 changed files with 134 additions and 119 deletions

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.library package eu.kanade.tachiyomi.data.library
import android.app.ActivityManager
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
@ -14,7 +15,6 @@ import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
@ -41,13 +42,17 @@ import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource import eu.kanade.tachiyomi.util.syncChaptersWithSource
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.ArrayList import java.util.ArrayList
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -94,6 +99,8 @@ class LibraryUpdateService(
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
} }
private var job:Job? = null
/** /**
* Cached progress notification to avoid creating a lot. * Cached progress notification to avoid creating a lot.
*/ */
@ -123,6 +130,8 @@ class LibraryUpdateService(
*/ */
const val KEY_CATEGORY = "category" const val KEY_CATEGORY = "category"
private val mangaToUpdate = mutableListOf<LibraryManga>()
/** /**
* Key that defines what should be updated. * Key that defines what should be updated.
*/ */
@ -158,6 +167,21 @@ class LibraryUpdateService(
context.startForegroundService(intent) context.startForegroundService(intent)
} }
} }
else {
if (target == Target.CHAPTERS) category?.id?.let {
val preferences: PreferencesHelper = Injekt.get()
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
addManga(getMangaToUpdate(it, target).sortedWith(
rankingScheme[selectedScheme]
))
}
}
}
private fun addManga(mangaToAdd: List<LibraryManga>) {
for (manga in mangaToAdd) {
if (mangaToUpdate.none { it.id == manga.id }) mangaToUpdate.add(manga)
}
} }
/** /**
@ -169,6 +193,38 @@ class LibraryUpdateService(
context.stopService(Intent(context, LibraryUpdateService::class.java)) context.stopService(Intent(context, LibraryUpdateService::class.java))
} }
/**
* Returns the list of manga to be updated.
*
* @param intent the update intent.
* @param target the target to update.
* @return a list of manga to update
*/
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> {
val preferences: PreferencesHelper = Injekt.get()
val db: DatabaseHelper = Injekt.get()
var listToUpdate = if (categoryId != -1)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else {
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
if (categoriesToUpdate.isNotEmpty())
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
else
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
}
return listToUpdate
}
private fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
return getMangaToUpdate(categoryId, target)
}
} }
/** /**
@ -183,12 +239,18 @@ class LibraryUpdateService(
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30)) wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
} }
override fun stopService(name: Intent?): Boolean {
job?.cancel()
return super.stopService(name)
}
/** /**
* Method called when the service is destroyed. It destroys subscriptions and releases the wake * Method called when the service is destroyed. It destroys subscriptions and releases the wake
* lock. * lock.
*/ */
override fun onDestroy() { override fun onDestroy() {
subscription?.unsubscribe() subscription?.unsubscribe()
mangaToUpdate.clear()
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
wakeLock.release() wakeLock.release()
} }
@ -212,77 +274,56 @@ class LibraryUpdateService(
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY if (intent == null) return START_NOT_STICKY
val target = intent.getSerializableExtra(KEY_TARGET) as? Target val target = intent.getSerializableExtra(KEY_TARGET) as? Target ?: return START_NOT_STICKY
?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed. // Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe() subscription?.unsubscribe()
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception) Timber.e(exception)
stopSelf(startId) stopSelf(startId)
} }
// Update either chapter list or manga details.
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
if (target == Target.CHAPTERS) {
updateChapters(
getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme]), startId
)
}
else {
// Update either chapter list or manga details.
// Update favorite manga. Destroy service when completed or in case of an error. // Update favorite manga. Destroy service when completed or in case of an error.
val mangaList = getMangaToUpdate(intent, target) val mangaList =
.sortedWith(rankingScheme[selectedScheme]) getMangaToUpdate(intent, target).sortedWith(rankingScheme[selectedScheme])
subscription = Observable.defer { subscription = Observable.defer {
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList(mangaList)
Target.DETAILS -> updateDetails(mangaList) Target.DETAILS -> updateDetails(mangaList)
else -> updateTrackings(mangaList) else -> updateTrackings(mangaList)
} }
}.subscribeOn(Schedulers.io()).subscribe({}, { }.subscribeOn(Schedulers.io()).subscribe({}, {
Timber.e(it) Timber.e(it)
stopSelf(startId) stopSelf(startId)
}, { }, {
stopSelf(startId) stopSelf(startId)
}) })
}
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
/** private fun updateChapters(mangaToAdd: List<LibraryManga>, startId: Int) {
* Returns the list of manga to be updated. addManga(mangaToAdd)
*
* @param intent the update intent.
* @param target the target to update.
* @return a list of manga to update
*/
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1) if (job == null) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
else { updateChaptersJob()
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt) mangaToUpdate.clear()
if (categoriesToUpdate.isNotEmpty()) stopSelf(startId)
db.getLibraryMangas().executeAsBlocking() }
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
else
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
} }
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
}
return listToUpdate
} }
/** private fun updateChaptersJob() {
* Method that updates the given list of manga. 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.
*
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) var count = 0
// List containing new updates // List containing new updates
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>() val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
// list containing failed updates // list containing failed updates
@ -294,64 +335,49 @@ class LibraryUpdateService(
// Boolean to determine if DownloadManager has downloads // Boolean to determine if DownloadManager has downloads
var hasDownloads = false var hasDownloads = false
// Emit each manga and update it sequentially. while (count < mangaToUpdate.size && count + 1 >= mangaToUpdate.size) {
return Observable.from(mangaToUpdate) if (job?.isCancelled == true || job == null) break
// Notify manga that will update. val manga = mangaToUpdate[count]
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } showProgressNotification(manga, count++, mangaToUpdate.size)
// Update the chapters of the manga. val source = sourceManager.get(manga.source) as? HttpSource ?: continue
.concatMap { manga -> val fetchedChapters = try { source.fetchChapterList(manga).toBlocking().single() }
updateManga(manga) catch(e: java.lang.Exception) {
// If there's any error, return empty update and continue. failedUpdates.add(manga)
.onErrorReturn { emptyList<SChapter>() }
failedUpdates.add(manga) if (fetchedChapters.isNotEmpty()) {
Pair(emptyList(), emptyList()) val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source).first
} if (newChapters.isNotEmpty()) {
// Filter out mangas without new chapters (or failed). if (downloadNew && (categoriesToDownload.isEmpty() || manga.category in categoriesToDownload)) {
.filter { pair -> pair.first.isNotEmpty() } downloadChapters(manga, newChapters)
.doOnNext { hasDownloads = true
if (downloadNew && (categoriesToDownload.isEmpty() || }
manga.category in categoriesToDownload)) { newUpdates.add(manga to newChapters.toTypedArray())
downloadChapters(manga, it.first)
hasDownloads = true
}
}
// Convert to the manga that contains new chapters.
.map { Pair(manga, (it.first.sortedByDescending { ch -> ch
.source_order }.toTypedArray())) }
} }
// Add manga with new chapters to the list. }
.doOnNext { manga -> }
// Add to the list if (newUpdates.isNotEmpty()) {
newUpdates.add(manga) showResultNotification(newUpdates)
}
// Notify result of the overall update.
.doOnCompleted {
if (newUpdates.isNotEmpty()) {
showResultNotification(newUpdates)
if (preferences.refreshCoversToo().getOrDefault()) { if (preferences.refreshCoversToo().getOrDefault()) {
updateDetails(newUpdates.map { it.first }).observeOn(Schedulers.io()) updateDetails(newUpdates.map { it.first }).observeOn(Schedulers.io())
.doOnCompleted { .doOnCompleted {
cancelProgressNotification() cancelProgressNotification()
if (downloadNew && hasDownloads) { if (downloadNew && hasDownloads) {
DownloadService.start(this)
}
}
.subscribeOn(Schedulers.io()).subscribe {}
}
else if (downloadNew && hasDownloads) {
DownloadService.start(this) DownloadService.start(this)
} }
} }
.subscribeOn(Schedulers.io()).subscribe {}
}
else if (downloadNew && hasDownloads) {
DownloadService.start(this)
}
}
if (failedUpdates.isNotEmpty()) { if (failedUpdates.isNotEmpty()) {
Timber.e("Failed updating: ${failedUpdates.map { it.title }}") Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
} }
cancelProgressNotification() cancelProgressNotification()
}
.map { manga -> manga.first }
} }
private fun cleanupDownloads() { private fun cleanupDownloads() {
@ -520,9 +546,6 @@ class LibraryUpdateService(
} }
NotificationManagerCompat.from(this).apply { NotificationManagerCompat.from(this).apply {
notifications.forEach {
notify(it.second, it.first)
}
notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) { notify(Notifications.ID_NEW_CHAPTERS, notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
@ -542,10 +565,15 @@ class LibraryUpdateService(
} }
priority = NotificationCompat.PRIORITY_HIGH priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS) setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setGroupSummary(true) setGroupSummary(true)
setContentIntent(getNotificationIntent()) setContentIntent(getNotificationIntent())
setAutoCancel(true) setAutoCancel(true)
}) })
notifications.forEach {
notify(it.second, it.first)
}
} }
} }

View File

@ -119,12 +119,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// Double the distance required to trigger sync // Double the distance required to trigger sync
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
swipe_refresh.setOnRefreshListener { swipe_refresh.setOnRefreshListener {
if (!LibraryUpdateService.isRunning(context)) { LibraryUpdateService.start(context, category)
LibraryUpdateService.start(context, category) controller.snack?.dismiss()
controller.snack?.dismiss() controller.snack = swipe_refresh.snack(R.string.updating_category)
controller.snack = swipe_refresh.snack(R.string.updating_category)
}
// It can be a very long operation, so we disable swipe refresh and show a toast.
swipe_refresh.isRefreshing = false swipe_refresh.isRefreshing = false
} }
} }

View File

@ -476,10 +476,6 @@ class LibraryController(
R.id.action_filter -> { R.id.action_filter -> {
navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) } navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) }
} }
R.id.action_update_library -> {
activity?.let { LibraryUpdateService.start(it) }
snack = view?.snack(R.string.updating_library)
}
R.id.action_edit_categories -> { R.id.action_edit_categories -> {
router.pushController(CategoryController().withFadeTransaction()) router.pushController(CategoryController().withFadeTransaction())
} }

View File

@ -16,12 +16,6 @@
android:title="@string/action_filter" android:title="@string/action_filter"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_update_library"
android:icon="@drawable/ic_refresh_white_24dp"
android:title="@string/action_update_library"
app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_edit_categories" android:id="@+id/action_edit_categories"
android:title="@string/action_edit_categories" android:title="@string/action_edit_categories"