mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-04 09:35:06 +01:00
Update restore to use a notification
Also added cleanup option to downloads
This commit is contained in:
parent
5368e37988
commit
b44ec4bfab
@ -32,6 +32,8 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -263,9 +265,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @param manga manga that needs updating
|
* @param manga manga that needs updating
|
||||||
* @return [Observable] that contains manga
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga {
|
||||||
return source.fetchMangaDetails(manga)
|
return withContext(Dispatchers.IO) {
|
||||||
.map { networkManga ->
|
val networkManga = source.fetchMangaDetails(manga).toBlocking().single()
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
manga.favorite = true
|
manga.favorite = true
|
||||||
manga.initialized = true
|
manga.initialized = true
|
||||||
@ -281,11 +283,12 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
* @param manga manga that needs updating
|
* @param manga manga that needs updating
|
||||||
* @return [Observable] that contains manga
|
* @return [Observable] that contains manga
|
||||||
*/
|
*/
|
||||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
suspend fun restoreChapterFetch(source: Source, manga: Manga, chapters: List<Chapter>) {
|
||||||
return source.fetchChapterList(manga)
|
withContext(Dispatchers.IO) {
|
||||||
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
val fetchChapters = source.fetchChapterList(manga).toBlocking().single()
|
||||||
.doOnNext {
|
val syncChaptersWithSource =
|
||||||
if (it.first.isNotEmpty()) {
|
syncChaptersWithSource(databaseHelper, fetchChapters, manga, source)
|
||||||
|
if (syncChaptersWithSource.first.isNotEmpty()) {
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
insertChapters(chapters)
|
insertChapters(chapters)
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -21,64 +26,28 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceNotFoundException
|
import eu.kanade.tachiyomi.source.SourceNotFoundException
|
||||||
import eu.kanade.tachiyomi.util.chop
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
import rx.Observable
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import rx.Subscription
|
import kotlinx.coroutines.GlobalScope
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup from json file
|
* Restores backup from json file
|
||||||
*/
|
*/
|
||||||
class BackupRestoreService : Service() {
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the status of the service.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
* @return true if the service is running, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun isRunning(context: Context): Boolean =
|
|
||||||
context.isServiceRunning(BackupRestoreService::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a service to restore a backup from Json
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
* @param uri path of Uri
|
|
||||||
*/
|
|
||||||
fun start(context: Context, uri: Uri) {
|
|
||||||
if (!isRunning(context)) {
|
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
|
||||||
}
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the service.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
fun stop(context: Context) {
|
|
||||||
context.stopService(Intent(context, BackupRestoreService::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wake lock that will be held until the service is destroyed.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
@ -88,27 +57,29 @@ class BackupRestoreService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Subscription where the update is done.
|
* Subscription where the update is done.
|
||||||
*/
|
*/
|
||||||
private var subscription: Subscription? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The progress of a backup restore
|
* The progress of a backup restore
|
||||||
*/
|
*/
|
||||||
private var restoreProgress = 0
|
private var restoreProgress = 0
|
||||||
|
|
||||||
/**
|
private var totalAmount = 0
|
||||||
* Amount of manga in Json file (needed for restore)
|
|
||||||
*/
|
|
||||||
private var restoreAmount = 0
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List containing errors
|
* List containing errors
|
||||||
*/
|
*/
|
||||||
private val errors = mutableListOf<Pair<Date, String>>()
|
private val errors = mutableListOf<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* count of cancelled
|
||||||
|
*/
|
||||||
|
private var cancelled = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List containing distinct errors
|
* List containing distinct errors
|
||||||
*/
|
*/
|
||||||
private val errorsMini = mutableListOf<String>()
|
private val trackingErrors = mutableListOf<String>()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,6 +87,11 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private val sourcesMissing = mutableListOf<Long>()
|
private val sourcesMissing = mutableListOf<Long>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing missing sources
|
||||||
|
*/
|
||||||
|
private var lincensedManga = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backup manager
|
* Backup manager
|
||||||
*/
|
*/
|
||||||
@ -132,17 +108,15 @@ class BackupRestoreService : Service() {
|
|||||||
internal val trackManager: TrackManager by injectLazy()
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
|
||||||
private lateinit var executor: ExecutorService
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
startForeground(Notifications.ID_RESTORE_PROGRESS, progressNotification.build())
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
||||||
wakeLock.acquire()
|
wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/)
|
||||||
executor = Executors.newSingleThreadExecutor()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,8 +124,7 @@ class BackupRestoreService : Service() {
|
|||||||
* releases the wake lock.
|
* releases the wake lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
job?.cancel()
|
||||||
executor.shutdown() // must be called after unsubscribe
|
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -177,30 +150,32 @@ class BackupRestoreService : Service() {
|
|||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
job?.cancel()
|
||||||
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
subscription = Observable.using(
|
Timber.e(exception)
|
||||||
{ db.lowLevel().beginTransaction() },
|
showErrorNotification(exception.message!!)
|
||||||
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
|
stopSelf(startId)
|
||||||
{ executor.execute { db.lowLevel().endTransaction() } })
|
}
|
||||||
.doAfterTerminate { stopSelf(startId) }
|
job = GlobalScope.launch(handler) {
|
||||||
.subscribeOn(Schedulers.from(executor))
|
restoreBackup(uri!!)
|
||||||
.subscribe()
|
}
|
||||||
|
job?.invokeOnCompletion { stopSelf(startId) }
|
||||||
|
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun stopService(name: Intent?): Boolean {
|
||||||
* Returns an [Observable] containing restore process.
|
job?.cancel()
|
||||||
*
|
if (wakeLock.isHeld) {
|
||||||
* @param uri restore file
|
wakeLock.release()
|
||||||
* @return [Observable<Manga>]
|
}
|
||||||
*/
|
return super.stopService(name)
|
||||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
}
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
return Observable.just(Unit)
|
/**
|
||||||
.map {
|
* Restore a backup json file
|
||||||
|
*/
|
||||||
|
private suspend fun restoreBackup(uri: Uri) {
|
||||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
val json = JsonParser().parse(reader).asJsonObject
|
val json = JsonParser().parse(reader).asJsonObject
|
||||||
|
|
||||||
@ -212,73 +187,127 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
// +1 for categories
|
||||||
restoreProgress = 0
|
totalAmount = mangasJson.size() + 1
|
||||||
|
trackingErrors.clear()
|
||||||
|
sourcesMissing.clear()
|
||||||
|
lincensedManga = 0
|
||||||
errors.clear()
|
errors.clear()
|
||||||
errorsMini.clear()
|
cancelled = 0
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
json.get(CATEGORIES)?.let {
|
restoreCategories(json, backupManager)
|
||||||
backupManager.restoreCategories(it.asJsonArray)
|
|
||||||
restoreProgress += 1
|
mangasJson.forEach {
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
restoreManga(it.asJsonObject, backupManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
mangasJson
|
notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
|
||||||
|
cancelled = errors.count { it.contains("cancelled", true) }
|
||||||
|
val tmpErrors = errors.filter { !it.contains("cancelled", true) }
|
||||||
|
errors.clear()
|
||||||
|
errors.addAll(tmpErrors)
|
||||||
|
|
||||||
|
val logFile = writeErrorLog()
|
||||||
|
showResultNotification(logFile.parent, logFile.name)
|
||||||
}
|
}
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.concatMap {
|
|
||||||
val obj = it.asJsonObject
|
/**Restore categories if they were backed up
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private fun restoreCategories(json: JsonObject, backupManager: BackupManager) {
|
||||||
|
val element = json.get(CATEGORIES)
|
||||||
|
if (element != null) {
|
||||||
|
backupManager.restoreCategories(element.asJsonArray)
|
||||||
|
restoreProgress += 1
|
||||||
|
showProgressNotification(restoreProgress, totalAmount, "Categories added")
|
||||||
|
} else {
|
||||||
|
totalAmount -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore manga from json this should be refactored more at some point to prevent the manga object from being mutable
|
||||||
|
*/
|
||||||
|
private suspend fun restoreManga(obj: JsonObject, backupManager: BackupManager) {
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
||||||
|
val source = backupManager.sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
try {
|
||||||
if (observable != null) {
|
if (job?.isCancelled == false) {
|
||||||
observable
|
showProgressNotification(restoreProgress, totalAmount, manga.title)
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
|
||||||
Observable.just(manga)
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
throw java.lang.Exception("Job was cancelled")
|
||||||
}
|
}
|
||||||
.toList()
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
.doOnNext {
|
val dbMangaExists = dbManga != null
|
||||||
val endTime = System.currentTimeMillis()
|
|
||||||
val time = endTime - startTime
|
|
||||||
val logFile = writeErrorLog()
|
|
||||||
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_TIME, time)
|
|
||||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
|
||||||
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
|
||||||
val sourceMissingCount = sourcesMissing.distinct().size
|
|
||||||
val sourceErrors = getString(R.string.sources_missing, sourceMissingCount)
|
|
||||||
val otherErrors = errorsMini.distinct().joinToString("\n")
|
|
||||||
putExtra(BackupConst.EXTRA_MINI_ERROR,
|
|
||||||
if (sourceMissingCount > 0) sourceErrors + "\n" + otherErrors
|
|
||||||
else otherErrors
|
|
||||||
)
|
|
||||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(completeIntent)
|
|
||||||
|
|
||||||
|
if (dbMangaExists) {
|
||||||
|
// Manga in database copy information from manga already in database
|
||||||
|
backupManager.restoreMangaNoFetch(manga, dbManga!!)
|
||||||
|
} else {
|
||||||
|
// manga gets details from network
|
||||||
|
backupManager.restoreMangaFetch(source, manga)
|
||||||
}
|
}
|
||||||
.doOnError { error ->
|
|
||||||
Timber.e(error)
|
if (!dbMangaExists || !backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
writeErrorLog()
|
//manga gets chapters added
|
||||||
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
backupManager.restoreChapterFetch(source, manga, chapters)
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
}
|
||||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(manga, categories)
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
|
||||||
|
trackingFetch(manga, tracks)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
|
||||||
|
if (e is RuntimeException) {
|
||||||
|
val cause = e.cause
|
||||||
|
if (cause is SourceNotFoundException) {
|
||||||
|
sourcesMissing.add(cause.id)
|
||||||
|
}
|
||||||
|
else if (e.message?.contains("licensed", true) == true) {
|
||||||
|
lincensedManga++
|
||||||
|
}
|
||||||
|
errors.add("${manga.title} - ${cause?.message ?: e.message}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errors.add("${manga.title} - ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [refreshes tracking information
|
||||||
|
* @param manga manga that needs updating.
|
||||||
|
* @param tracks list containing tracks from restore file.
|
||||||
|
*/
|
||||||
|
private fun trackingFetch(manga: Manga, tracks: List<Track>) {
|
||||||
|
tracks.forEach { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged) {
|
||||||
|
service.refresh(track)
|
||||||
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add("${manga.title} - ${it.message}")
|
||||||
|
track
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.add("${manga.title} - ${service?.name} not logged in")
|
||||||
|
val notLoggedIn = getString(R.string.not_logged_into, service?.name)
|
||||||
|
trackingErrors.add(notLoggedIn)
|
||||||
}
|
}
|
||||||
sendLocalBroadcast(errorIntent)
|
|
||||||
}
|
}
|
||||||
.onErrorReturn { emptyList() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -291,186 +320,149 @@ class BackupRestoreService : Service() {
|
|||||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
destFile.bufferedWriter().use { out ->
|
destFile.bufferedWriter().use { out ->
|
||||||
errors.forEach { (date, message) ->
|
errors.forEach { message ->
|
||||||
out.write("[${sdf.format(date)}] $message\n")
|
out.write("$message\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return destFile
|
return destFile
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Empty
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
return File("")
|
return File("")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a manga restore observable
|
* keep a partially constructed progress notification for resuse
|
||||||
*
|
|
||||||
* @param manga manga data from json
|
|
||||||
* @param chapters chapters data from json
|
|
||||||
* @param categories categories data from json
|
|
||||||
* @param history history data from json
|
|
||||||
* @param tracks tracking data from json
|
|
||||||
* @return [Observable] containing manga restore information
|
|
||||||
*/
|
*/
|
||||||
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
private val progressNotification by lazy {
|
||||||
categories: List<String>, history: List<DHistory>,
|
NotificationCompat.Builder(this, Notifications.CHANNEL_RESTORE)
|
||||||
tracks: List<Track>): Observable<Manga>? {
|
.setContentTitle(getString(R.string.app_name))
|
||||||
// Get source
|
.setSmallIcon(R.drawable.ic_tachi)
|
||||||
val source = backupManager.sourceManager.getOrStub(manga.source)
|
.setOngoing(true)
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
.setOnlyAlertOnce(true)
|
||||||
|
.setAutoCancel(false)
|
||||||
return if (dbManga == null) {
|
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||||
// Manga not in database
|
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||||
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
|
||||||
} else { // Manga in database
|
|
||||||
// Copy information from manga already in database
|
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
|
||||||
// Fetch rest of manga information
|
|
||||||
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Observable] that fetches manga information
|
* Pending intent of action that cancels the library update
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @param chapters chapters of manga that needs updating
|
|
||||||
* @param categories categories that need updating
|
|
||||||
*/
|
*/
|
||||||
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
private val cancelIntent by lazy {
|
||||||
categories: List<String>, history: List<DHistory>,
|
NotificationReceiver.cancelRestorePendingBroadcast(this)
|
||||||
tracks: List<Track>): Observable<Manga> {
|
|
||||||
return backupManager.restoreMangaFetchObservable(source, manga)
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
if (it is SourceNotFoundException) {
|
|
||||||
sourcesMissing.add(it.id)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
errorsMini.add(it.message ?: "")
|
|
||||||
}
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.filter { it.id != null }
|
|
||||||
.flatMap {
|
|
||||||
chapterFetchObservable(source, it, chapters)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
trackingFetchObservable(it, tracks)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
|
||||||
categories: List<String>, history: List<DHistory>,
|
|
||||||
tracks: List<Track>): Observable<Manga> {
|
|
||||||
|
|
||||||
return Observable.just(backupManga)
|
/**
|
||||||
.flatMap { manga ->
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
*
|
||||||
chapterFetchObservable(source, manga, chapters)
|
* @param manga the manga that's being updated.
|
||||||
.map { manga }
|
* @param current the current progress.
|
||||||
|
* @param total the total progress.
|
||||||
|
*/
|
||||||
|
private fun showProgressNotification(current: Int, total: Int, title: String) {
|
||||||
|
notificationManager.notify(Notifications.ID_RESTORE_PROGRESS, progressNotification
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setProgress(total, current, false)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the result notification with option to show the error log
|
||||||
|
*/
|
||||||
|
private fun showResultNotification(path: String?, file: String?) {
|
||||||
|
|
||||||
|
val content = mutableListOf(getString(R.string.restore_completed_content, restoreProgress
|
||||||
|
.toString(), errors.size.toString()))
|
||||||
|
val sourceMissingCount = sourcesMissing.distinct().size
|
||||||
|
if (sourceMissingCount > 0)
|
||||||
|
content.add(getString(R.string.sources_missing, sourceMissingCount))
|
||||||
|
if (lincensedManga > 0)
|
||||||
|
content.add(getString(R.string.x_licensed_manga, lincensedManga))
|
||||||
|
val trackingErrors = trackingErrors.distinct()
|
||||||
|
if (trackingErrors.isNotEmpty()) {
|
||||||
|
val trackingErrorsString = trackingErrors.distinct().joinToString("\n")
|
||||||
|
content.add(trackingErrorsString)
|
||||||
|
}
|
||||||
|
if (cancelled > 0)
|
||||||
|
content.add(getString(R.string.restore_completed_content_2, cancelled))
|
||||||
|
|
||||||
|
val restoreString = content.joinToString("\n")
|
||||||
|
|
||||||
|
val resultNotification = NotificationCompat.Builder(this, Notifications.CHANNEL_RESTORE)
|
||||||
|
.setContentTitle(getString(R.string.restore_completed))
|
||||||
|
.setContentText(restoreString)
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(restoreString))
|
||||||
|
.setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||||
|
if (errors.size > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||||
|
resultNotification.addAction(R.drawable.ic_clear_grey_24dp_img, getString(R.string
|
||||||
|
.notification_action_error_log), getErrorLogIntent(path, file))
|
||||||
|
}
|
||||||
|
notificationManager.notify(Notifications.ID_RESTORE_COMPLETE, resultNotification.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Show an error notification if something happens that prevents the restore from starting/working
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private fun showErrorNotification(errorMessage: String) {
|
||||||
|
val resultNotification = NotificationCompat.Builder(this, Notifications.CHANNEL_RESTORE)
|
||||||
|
.setContentTitle(getString(R.string.restore_error))
|
||||||
|
.setContentText(errorMessage)
|
||||||
|
.setSmallIcon(R.drawable.ic_error_grey)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.md_red_500))
|
||||||
|
notificationManager.notify(Notifications.ID_RESTORE_ERROR, resultNotification.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Get the PendingIntent for the error log
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private fun getErrorLogIntent(path: String, file: String): PendingIntent {
|
||||||
|
val destFile = File(path, file!!)
|
||||||
|
val uri = destFile.getUriCompat(applicationContext)
|
||||||
|
return NotificationReceiver.openFileExplorerPendingActivity(this@BackupRestoreService, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isRunning(context: Context): Boolean =
|
||||||
|
context.isServiceRunning(BackupRestoreService::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a service to restore a backup from Json
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri path of Uri
|
||||||
|
*/
|
||||||
|
fun start(context: Context, uri: Uri) {
|
||||||
|
if (!isRunning(context)) {
|
||||||
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
} else {
|
} else {
|
||||||
Observable.just(manga)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnNext {
|
|
||||||
restoreExtraForManga(it, categories, history, tracks)
|
|
||||||
}
|
|
||||||
.flatMap { manga ->
|
|
||||||
trackingFetchObservable(manga, tracks)
|
|
||||||
// Convert to the manga that contains new chapters.
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
.doOnCompleted {
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
|
||||||
// Restore categories
|
|
||||||
backupManager.restoreCategoriesForManga(manga, categories)
|
|
||||||
|
|
||||||
// Restore history
|
|
||||||
backupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(manga, tracks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Observable] that fetches chapter information
|
* Stops the service.
|
||||||
*
|
*
|
||||||
* @param source source of manga
|
* @param context the application context.
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return [Observable] that contains manga
|
|
||||||
*/
|
*/
|
||||||
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun stop(context: Context) {
|
||||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||||
// If there's any error, return empty update and continue.
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
errorsMini.add(it.message ?: "")
|
|
||||||
Pair(emptyList(), emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [Observable] that refreshes tracking information
|
|
||||||
* @param manga manga that needs updating.
|
|
||||||
* @param tracks list containing tracks from restore file.
|
|
||||||
* @return [Observable] that contains updated track item
|
|
||||||
*/
|
|
||||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
|
||||||
return Observable.from(tracks)
|
|
||||||
.concatMap { track ->
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
service.refresh(track)
|
|
||||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
|
||||||
.onErrorReturn {
|
|
||||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
|
||||||
errorsMini.add(it.message ?: "")
|
|
||||||
track
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val notLoggedIn = getString(R.string.not_logged_into, service?.name)
|
|
||||||
errors.add(Date() to "${manga.title} - $notLoggedIn")
|
|
||||||
errorsMini.add(notLoggedIn)
|
|
||||||
Observable.empty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update dialog in [BackupConst]
|
|
||||||
*
|
|
||||||
* @param progress restore progress
|
|
||||||
* @param amount total restoreAmount of manga
|
|
||||||
* @param title title of restored manga
|
|
||||||
*/
|
|
||||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
|
||||||
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
|
||||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
|
||||||
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
|
||||||
putExtra(BackupConst.EXTRA_CONTENT, content)
|
|
||||||
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
|
||||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
|
||||||
}
|
|
||||||
sendLocalBroadcast(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -212,6 +212,25 @@ class DownloadManager(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the directories of chapters that were read or have no match
|
||||||
|
*
|
||||||
|
* @param chapters the list of chapters to delete.
|
||||||
|
* @param manga the manga of the chapters.
|
||||||
|
* @param source the source of the chapters.
|
||||||
|
*/
|
||||||
|
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source) {
|
||||||
|
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||||
|
filesWithNoChapter.forEach { it.delete() }
|
||||||
|
val readChapters = allChapters.filter { it.read }
|
||||||
|
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
||||||
|
readChapterDirs.forEach { it.delete() }
|
||||||
|
cache.removeChapters(readChapters, manga)
|
||||||
|
if (cache.getDownloadCount(manga) == 0) {
|
||||||
|
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete()// Delete manga directory if empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the directory of a downloaded manga.
|
* Deletes the directory of a downloaded manga.
|
||||||
*
|
*
|
||||||
|
@ -100,6 +100,26 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return chapters.flatMap { getValidChapterDirNames(it) }.mapNotNull { mangaDir.findFile(it) }
|
return chapters.flatMap { getValidChapterDirNames(it) }.mapNotNull { mangaDir.findFile(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all files in manga directory
|
||||||
|
*
|
||||||
|
* @param chapters the chapters to query.
|
||||||
|
* @param manga the manga of the chapter.
|
||||||
|
* @param source the source of the chapter.
|
||||||
|
*/
|
||||||
|
fun findUnmatchedChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
||||||
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
|
return mangaDir.listFiles()!!.asList().filter {
|
||||||
|
chapters.find { chp ->
|
||||||
|
(getValidChapterDirNames(chp) + "${getChapterDirName(chp)}_tmp").any { dir ->
|
||||||
|
mangaDir.findFile(
|
||||||
|
dir
|
||||||
|
) != null
|
||||||
|
}
|
||||||
|
} == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of downloaded directories for the chapters that exist.
|
* Returns a list of downloaded directories for the chapters that exist.
|
||||||
*
|
*
|
||||||
|
@ -37,6 +37,10 @@ import eu.kanade.tachiyomi.util.isServiceRunning
|
|||||||
import eu.kanade.tachiyomi.util.notification
|
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.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
|
||||||
@ -72,6 +76,9 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
var job: Job? = null
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pending intent of action that cancels the library update
|
* Pending intent of action that cancels the library update
|
||||||
*/
|
*/
|
||||||
@ -105,7 +112,8 @@ class LibraryUpdateService(
|
|||||||
enum class Target {
|
enum class Target {
|
||||||
CHAPTERS, // Manga chapters
|
CHAPTERS, // Manga chapters
|
||||||
DETAILS, // Manga metadata
|
DETAILS, // Manga metadata
|
||||||
TRACKING // Tracking metadata
|
TRACKING, // Tracking metadata
|
||||||
|
CLEANUP // Clean up downloads
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -203,37 +211,43 @@ class LibraryUpdateService(
|
|||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (intent == null) return Service.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 Service.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()
|
||||||
|
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
|
||||||
subscription = Observable
|
|
||||||
.defer {
|
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||||
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
val mangaList = getMangaToUpdate(intent, target)
|
val mangaList = getMangaToUpdate(intent, target)
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
.sortedWith(rankingScheme[selectedScheme])
|
||||||
|
|
||||||
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
|
Timber.e(exception)
|
||||||
|
stopSelf(startId)
|
||||||
|
}
|
||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
|
if (target == Target.CLEANUP) {
|
||||||
|
job = GlobalScope.launch(handler) {
|
||||||
|
cleanupDownloads()
|
||||||
|
}
|
||||||
|
job?.invokeOnCompletion { stopSelf(startId) }
|
||||||
|
} else {
|
||||||
|
subscription = Observable.defer {
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
Target.DETAILS -> updateDetails(mangaList)
|
Target.DETAILS -> updateDetails(mangaList)
|
||||||
Target.TRACKING -> 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 Service.START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -336,6 +350,15 @@ class LibraryUpdateService(
|
|||||||
.map { manga -> manga.first }
|
.map { manga -> manga.first }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanupDownloads() {
|
||||||
|
val mangaList = db.getMangas().executeAsBlocking()
|
||||||
|
for (manga in mangaList) {
|
||||||
|
val chapterList = db.getChapters(manga).executeAsBlocking()
|
||||||
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
downloadManager.cleanupChapters(chapterList, manga, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
// we need to get the chapters from the db so we have chapter ids
|
// we need to get the chapters from the db so we have chapter ids
|
||||||
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
@ -7,9 +7,11 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -31,6 +33,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global [BroadcastReceiver] that runs on UI thread
|
* Global [BroadcastReceiver] that runs on UI thread
|
||||||
* Pending Broadcasts should be made from here.
|
* Pending Broadcasts should be made from here.
|
||||||
@ -65,6 +68,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
// Cancel library update and dismiss notification
|
// Cancel library update and dismiss notification
|
||||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
||||||
|
ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context)
|
||||||
// Open reader activity
|
// Open reader activity
|
||||||
ACTION_OPEN_CHAPTER -> {
|
ACTION_OPEN_CHAPTER -> {
|
||||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
@ -164,7 +168,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when user wants to stop a library update
|
* Method called when user wants to mark as read
|
||||||
*
|
*
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
@ -185,6 +189,16 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Method called when user wants to stop a restore
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
*/
|
||||||
|
private fun cancelRestoreUpdate(context: Context) {
|
||||||
|
BackupRestoreService.stop(context)
|
||||||
|
Handler().post { dismissNotification(context, Notifications.ID_RESTORE_PROGRESS) }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
@ -197,9 +211,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Called to cancel library update.
|
// Called to cancel library update.
|
||||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||||
|
|
||||||
// Called to cancel library update.
|
// Called to mark as read
|
||||||
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||||
|
|
||||||
|
// Called to cancel restore
|
||||||
|
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
||||||
|
|
||||||
|
|
||||||
// Called to open chapter
|
// Called to open chapter
|
||||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
|
||||||
@ -410,6 +428,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**Returns the PendingIntent that will open the error log in an external text viewer
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
internal fun openFileExplorerPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
|
val toLaunch = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "text/plain")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, toLaunch, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
|
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
|
||||||
*
|
*
|
||||||
@ -441,5 +472,18 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a service which stops the restore service
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun cancelRestorePendingBroadcast(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_CANCEL_RESTORE
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -44,6 +44,12 @@ object Notifications {
|
|||||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
|
|
||||||
|
const val CHANNEL_RESTORE = "backup_restore_channel"
|
||||||
|
const val ID_RESTORE_PROGRESS = -501
|
||||||
|
const val ID_RESTORE_COMPLETE = -502
|
||||||
|
const val ID_RESTORE_ERROR = -503
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the notification channels introduced in Android Oreo.
|
* Creates the notification channels introduced in Android Oreo.
|
||||||
*
|
*
|
||||||
@ -77,7 +83,10 @@ object Notifications {
|
|||||||
CHANNEL_NEW_CHAPTERS,
|
CHANNEL_NEW_CHAPTERS,
|
||||||
context.getString(R.string.channel_new_chapters),
|
context.getString(R.string.channel_new_chapters),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
)
|
), NotificationChannel(CHANNEL_RESTORE, context.getString(R.string.channel_backup_restore),
|
||||||
|
NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
|
setShowBadge(false)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
context.notificationManager.createNotificationChannels(channels)
|
context.notificationManager.createNotificationChannels(channels)
|
||||||
}
|
}
|
||||||
|
@ -176,8 +176,7 @@ class ChaptersPresenter(
|
|||||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
||||||
if (onlyUnread()) {
|
if (onlyUnread()) {
|
||||||
observable = observable.filter { !it.read }
|
observable = observable.filter { !it.read }
|
||||||
}
|
} else if (onlyRead()) {
|
||||||
else if (onlyRead()) {
|
|
||||||
observable = observable.filter { it.read }
|
observable = observable.filter { it.read }
|
||||||
}
|
}
|
||||||
if (onlyDownloaded()) {
|
if (onlyDownloaded()) {
|
||||||
|
@ -69,6 +69,13 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
|
|
||||||
onClick { LibraryUpdateService.start(context, target = Target.TRACKING) }
|
onClick { LibraryUpdateService.start(context, target = Target.TRACKING) }
|
||||||
}
|
}
|
||||||
|
preference {
|
||||||
|
titleRes = R.string.pref_clean_downloads
|
||||||
|
|
||||||
|
summaryRes = R.string.pref_clean_downloads_summary
|
||||||
|
|
||||||
|
onClick { LibraryUpdateService.start(context, target = Target.CLEANUP) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearChapterCache() {
|
private fun clearChapterCache() {
|
||||||
|
@ -288,7 +288,7 @@ class SettingsBackupController : SettingsController() {
|
|||||||
.onPositive { _, _ ->
|
.onPositive { _, _ ->
|
||||||
val context = applicationContext
|
val context = applicationContext
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
RestoringBackupDialog().showDialog(router, TAG_RESTORING_BACKUP_DIALOG)
|
activity?.toast(R.string.restoring_backup)
|
||||||
BackupRestoreService.start(context, args.getParcelable(KEY_URI)!!)
|
BackupRestoreService.start(context, args.getParcelable(KEY_URI)!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
app/src/main/res/drawable/ic_error_grey.xml
Normal file
5
app/src/main/res/drawable/ic_error_grey.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#898989"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||||
|
</vector>
|
@ -234,7 +234,7 @@
|
|||||||
<string name="restore_completed">S\'ha completat la restauració</string>
|
<string name="restore_completed">S\'ha completat la restauració</string>
|
||||||
<string name="error_opening_log">No s\'ha pogut obrir el registre</string>
|
<string name="error_opening_log">No s\'ha pogut obrir el registre</string>
|
||||||
<string name="restore_completed_content">La restauració ha tardat %1$s.
|
<string name="restore_completed_content">La restauració ha tardat %1$s.
|
||||||
\nS\'han produït $2$s errors.</string>
|
\nS\'han produït %2$s errors.</string>
|
||||||
<string name="backup_restore_content">La restauració obté dades de les fonts, poden haver-hi despeses de l\'operador.
|
<string name="backup_restore_content">La restauració obté dades de les fonts, poden haver-hi despeses de l\'operador.
|
||||||
\nAssegureu-vos que heu iniciat la sessió a les fonts en què cal abans de restaurar.</string>
|
\nAssegureu-vos que heu iniciat la sessió a les fonts en què cal abans de restaurar.</string>
|
||||||
<string name="file_saved">S\'ha desat el fitxer a %1$s</string>
|
<string name="file_saved">S\'ha desat el fitxer a %1$s</string>
|
||||||
|
@ -306,13 +306,18 @@
|
|||||||
<string name="dialog_restoring_source_not_found">Restoring backup\n%1$s source not found</string>
|
<string name="dialog_restoring_source_not_found">Restoring backup\n%1$s source not found</string>
|
||||||
<string name="backup_created">Backup created</string>
|
<string name="backup_created">Backup created</string>
|
||||||
<string name="restore_completed">Restore completed</string>
|
<string name="restore_completed">Restore completed</string>
|
||||||
|
<string name="restore_error">Restore error</string>
|
||||||
<string name="error_opening_log">Could not open log</string>
|
<string name="error_opening_log">Could not open log</string>
|
||||||
<string name="restore_completed_content">Restore took %1$s.\n%2$s errors found.</string>
|
<string name="restore_completed_content">Restored %1$s. %2$s errors found</string>
|
||||||
<string name="backup_restore_content">Restore uses source to fetch data, carrier costs may apply.\n\nMake sure you have installed all necessary extensions and are logged in to sources and tracking services before restoring.</string>
|
<string name="restore_completed_content_2">%1$d skipped</string>
|
||||||
|
<string name="backup_restore_content">Restore uses the network to fetch data, carrier costs may apply.\n\nMake sure you have installed all necessary extensions and are logged in to sources and tracking services before restoring.</string>
|
||||||
<string name="file_saved">File saved at %1$s</string>
|
<string name="file_saved">File saved at %1$s</string>
|
||||||
<string name="backup_choice">What do you want to backup?</string>
|
<string name="backup_choice">What do you want to backup?</string>
|
||||||
<string name="restoring_backup">Restoring backup</string>
|
<string name="restoring_backup">Restoring backup</string>
|
||||||
<string name="creating_backup">Creating backup</string>
|
<string name="creating_backup">Creating backup</string>
|
||||||
|
<string name="sources_missing">Sources missing: %1$d</string>
|
||||||
|
<string name="x_licensed_manga">%1$d manga are now licensed and could not be restored</string>
|
||||||
|
<string name="not_logged_into">Not logged into %1$s</string>
|
||||||
|
|
||||||
<!-- Advanced section -->
|
<!-- Advanced section -->
|
||||||
<string name="pref_clear_chapter_cache">Clear chapter cache</string>
|
<string name="pref_clear_chapter_cache">Clear chapter cache</string>
|
||||||
@ -330,6 +335,8 @@
|
|||||||
<string name="pref_refresh_library_metadata_summary">Updates covers, genres, description and manga status information</string>
|
<string name="pref_refresh_library_metadata_summary">Updates covers, genres, description and manga status information</string>
|
||||||
<string name="pref_refresh_library_tracking">Refresh tracking metadata</string>
|
<string name="pref_refresh_library_tracking">Refresh tracking metadata</string>
|
||||||
<string name="pref_refresh_library_tracking_summary">Updates status, score and last chapter read from the tracking services</string>
|
<string name="pref_refresh_library_tracking_summary">Updates status, score and last chapter read from the tracking services</string>
|
||||||
|
<string name="pref_clean_downloads">Clean up downloaded chapters</string>
|
||||||
|
<string name="pref_clean_downloads_summary">Deletes orphaned,tmp, and read chapter folders for entire library for each Manga</string>
|
||||||
|
|
||||||
<!-- About section -->
|
<!-- About section -->
|
||||||
<string name="version">Version</string>
|
<string name="version">Version</string>
|
||||||
@ -406,8 +413,6 @@
|
|||||||
<string name="delete_downloads_for_manga">Delete downloaded chapters?</string>
|
<string name="delete_downloads_for_manga">Delete downloaded chapters?</string>
|
||||||
<string name="copied_to_clipboard">%1$s copied to clipboard</string>
|
<string name="copied_to_clipboard">%1$s copied to clipboard</string>
|
||||||
<string name="source_not_installed">Source not installed: %1$s</string>
|
<string name="source_not_installed">Source not installed: %1$s</string>
|
||||||
<string name="sources_missing">Sources missing: %1$d</string>
|
|
||||||
<string name="not_logged_into">Not logged into %1$s</string>
|
|
||||||
|
|
||||||
<!-- Manga chapters fragment -->
|
<!-- Manga chapters fragment -->
|
||||||
<string name="manga_chapters_tab">Chapters</string>
|
<string name="manga_chapters_tab">Chapters</string>
|
||||||
@ -534,6 +539,8 @@
|
|||||||
<string name="notification_no_connection_title">Sync canceled</string>
|
<string name="notification_no_connection_title">Sync canceled</string>
|
||||||
<string name="notification_no_connection_body">Connection not available</string>
|
<string name="notification_no_connection_body">Connection not available</string>
|
||||||
|
|
||||||
|
<string name="notification_action_error_log">View all errors</string>
|
||||||
|
|
||||||
<!-- File Picker Titles -->
|
<!-- File Picker Titles -->
|
||||||
<string name="file_select_cover">Select cover image</string>
|
<string name="file_select_cover">Select cover image</string>
|
||||||
<string name="file_select_backup">Select backup file</string>
|
<string name="file_select_backup">Select backup file</string>
|
||||||
@ -581,6 +588,7 @@
|
|||||||
<string name="channel_library">Library</string>
|
<string name="channel_library">Library</string>
|
||||||
<string name="channel_downloader">Downloader</string>
|
<string name="channel_downloader">Downloader</string>
|
||||||
<string name="channel_ext_updates">Extension Updates</string>
|
<string name="channel_ext_updates">Extension Updates</string>
|
||||||
|
<string name="channel_backup_restore">Restore Backup</string>
|
||||||
<string name="channel_library_updates">Updating Library</string>
|
<string name="channel_library_updates">Updating Library</string>
|
||||||
<string name="channel_new_chapters">New Chapters</string>
|
<string name="channel_new_chapters">New Chapters</string>
|
||||||
|
|
||||||
|
@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.fail
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@ -23,7 +26,6 @@ import org.mockito.Mockito.*
|
|||||||
import org.robolectric.RuntimeEnvironment
|
import org.robolectric.RuntimeEnvironment
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.observers.TestSubscriber
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
@ -201,13 +203,16 @@ class BackupTest {
|
|||||||
// Restore manga with fetch observable
|
// Restore manga with fetch observable
|
||||||
val networkManga = getSingleManga("One Piece")
|
val networkManga = getSingleManga("One Piece")
|
||||||
networkManga.description = "This is a description"
|
networkManga.description = "This is a description"
|
||||||
`when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga))
|
`when`(source.fetchMangaDetailsObservable(jsonManga)).thenReturn(Observable.just(networkManga))
|
||||||
|
|
||||||
val obs = backupManager.restoreMangaFetchObservable(source, jsonManga)
|
GlobalScope.launch {
|
||||||
val testSubscriber = TestSubscriber<Manga>()
|
try {
|
||||||
obs.subscribe(testSubscriber)
|
backupManager.restoreMangaFetch(source, jsonManga)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
fail("Unexpected onError events")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
testSubscriber.assertNoErrors()
|
|
||||||
|
|
||||||
// Check if restore successful
|
// Check if restore successful
|
||||||
val dbCats = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
val dbCats = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
@ -245,14 +250,16 @@ class BackupTest {
|
|||||||
// Create list
|
// Create list
|
||||||
val chaptersRemote = ArrayList<Chapter>()
|
val chaptersRemote = ArrayList<Chapter>()
|
||||||
(1..10).mapTo(chaptersRemote) { getSingleChapter("Chapter $it") }
|
(1..10).mapTo(chaptersRemote) { getSingleChapter("Chapter $it") }
|
||||||
`when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote))
|
`when`(source.fetchChapterListObservable(manga)).thenReturn(Observable.just(chaptersRemote))
|
||||||
|
|
||||||
// Call restoreChapterFetchObservable
|
// Call restoreChapterFetchObservable
|
||||||
val obs = backupManager.restoreChapterFetchObservable(source, manga, restoredChapters)
|
GlobalScope.launch {
|
||||||
val testSubscriber = TestSubscriber<Pair<List<Chapter>, List<Chapter>>>()
|
try {
|
||||||
obs.subscribe(testSubscriber)
|
backupManager.restoreChapterFetch(source, manga, restoredChapters)
|
||||||
|
} catch (e: Exception) {
|
||||||
testSubscriber.assertNoErrors()
|
fail("Unexpected onError events")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val dbCats = backupManager.databaseHelper.getChapters(manga).executeAsBlocking()
|
val dbCats = backupManager.databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
assertThat(dbCats).hasSize(10)
|
assertThat(dbCats).hasSize(10)
|
||||||
|
Loading…
Reference in New Issue
Block a user