From b44ec4bfabca9ff59fdeae540c502816086ac27c Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 10 Jan 2020 23:31:29 -0800 Subject: [PATCH] Update restore to use a notification Also added cleanup option to downloads --- .../tachiyomi/data/backup/BackupManager.kt | 39 +- .../data/backup/BackupRestoreService.kt | 618 +++++++++--------- .../data/download/DownloadManager.kt | 19 + .../data/download/DownloadProvider.kt | 20 + .../data/library/LibraryUpdateService.kt | 61 +- .../data/notification/NotificationReceiver.kt | 48 +- .../data/notification/Notifications.kt | 11 +- .../ui/manga/chapter/ChaptersPresenter.kt | 3 +- .../ui/manga/info/MangaInfoPresenter.kt | 4 +- .../ui/setting/SettingsAdvancedController.kt | 7 + .../ui/setting/SettingsBackupController.kt | 2 +- app/src/main/res/drawable/ic_error_grey.xml | 5 + app/src/main/res/values-ca/strings.xml | 2 +- app/src/main/res/values/strings.xml | 16 +- .../tachiyomi/data/backup/BackupTest.kt | 41 +- 15 files changed, 516 insertions(+), 380 deletions(-) create mode 100644 app/src/main/res/drawable/ic_error_grey.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index b5a8e53a22..1398db0c25 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -32,6 +32,8 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.util.sendLocalBroadcast import eu.kanade.tachiyomi.util.syncChaptersWithSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import rx.Observable import timber.log.Timber import uy.kohesive.injekt.injectLazy @@ -263,15 +265,15 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { * @param manga manga that needs updating * @return [Observable] that contains manga */ - fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable { - return source.fetchMangaDetails(manga) - .map { networkManga -> - manga.copyFrom(networkManga) - manga.favorite = true - manga.initialized = true - manga.id = insertManga(manga) - manga - } + suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga { + return withContext(Dispatchers.IO) { + val networkManga = source.fetchMangaDetails(manga).toBlocking().single() + manga.copyFrom(networkManga) + manga.favorite = true + manga.initialized = true + manga.id = insertManga(manga) + manga + } } /** @@ -281,15 +283,16 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { * @param manga manga that needs updating * @return [Observable] that contains manga */ - fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List): Observable, List>> { - return source.fetchChapterList(manga) - .map { syncChaptersWithSource(databaseHelper, it, manga, source) } - .doOnNext { - if (it.first.isNotEmpty()) { - chapters.forEach { it.manga_id = manga.id } - insertChapters(chapters) - } - } + suspend fun restoreChapterFetch(source: Source, manga: Manga, chapters: List) { + withContext(Dispatchers.IO) { + val fetchChapters = source.fetchChapterList(manga).toBlocking().single() + val syncChaptersWithSource = + syncChaptersWithSource(databaseHelper, fetchChapters, manga, source) + if (syncChaptersWithSource.first.isNotEmpty()) { + chapters.forEach { it.manga_id = manga.id } + insertChapters(chapters) + } + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index fa4206989c..de4f14e47c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt @@ -1,13 +1,18 @@ package eu.kanade.tachiyomi.data.backup +import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.os.IBinder import android.os.PowerManager +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import com.github.salomonbrys.kotson.fromJson import com.google.gson.JsonArray +import com.google.gson.JsonObject import com.google.gson.JsonParser import com.google.gson.stream.JsonReader 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.database.DatabaseHelper 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.source.Source 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.sendLocalBroadcast -import rx.Observable -import rx.Subscription -import rx.schedulers.Schedulers +import eu.kanade.tachiyomi.util.notificationManager +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File import java.text.SimpleDateFormat -import java.util.Date import java.util.Locale -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors /** * Restores backup from json file */ 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. @@ -88,27 +57,29 @@ class BackupRestoreService : Service() { /** * Subscription where the update is done. */ - private var subscription: Subscription? = null + private var job: Job? = null /** * The progress of a backup restore */ private var restoreProgress = 0 - /** - * Amount of manga in Json file (needed for restore) - */ - private var restoreAmount = 0 + private var totalAmount = 0 /** * List containing errors */ - private val errors = mutableListOf>() + private val errors = mutableListOf() + + /** + * count of cancelled + */ + private var cancelled = 0 /** * List containing distinct errors */ - private val errorsMini = mutableListOf() + private val trackingErrors = mutableListOf() /** @@ -116,6 +87,11 @@ class BackupRestoreService : Service() { */ private val sourcesMissing = mutableListOf() + /** + * List containing missing sources + */ + private var lincensedManga = 0 + /** * Backup manager */ @@ -132,17 +108,15 @@ class BackupRestoreService : Service() { 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. */ override fun onCreate() { super.onCreate() + startForeground(Notifications.ID_RESTORE_PROGRESS, progressNotification.build()) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock") - wakeLock.acquire() - executor = Executors.newSingleThreadExecutor() + wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/) } /** @@ -150,8 +124,7 @@ class BackupRestoreService : Service() { * releases the wake lock. */ override fun onDestroy() { - subscription?.unsubscribe() - executor.shutdown() // must be called after unsubscribe + job?.cancel() if (wakeLock.isHeld) { wakeLock.release() } @@ -177,108 +150,164 @@ class BackupRestoreService : Service() { val uri = intent.getParcelableExtra(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY // Unsubscribe from any previous subscription if needed. - subscription?.unsubscribe() - - subscription = Observable.using( - { db.lowLevel().beginTransaction() }, - { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, - { executor.execute { db.lowLevel().endTransaction() } }) - .doAfterTerminate { stopSelf(startId) } - .subscribeOn(Schedulers.from(executor)) - .subscribe() + job?.cancel() + val handler = CoroutineExceptionHandler { _, exception -> + Timber.e(exception) + showErrorNotification(exception.message!!) + stopSelf(startId) + } + job = GlobalScope.launch(handler) { + restoreBackup(uri!!) + } + job?.invokeOnCompletion { stopSelf(startId) } return START_NOT_STICKY } + override fun stopService(name: Intent?): Boolean { + job?.cancel() + if (wakeLock.isHeld) { + wakeLock.release() + } + return super.stopService(name) + } + /** - * Returns an [Observable] containing restore process. - * - * @param uri restore file - * @return [Observable] + * Restore a backup json file */ - private fun getRestoreObservable(uri: Uri): Observable> { - val startTime = System.currentTimeMillis() + private suspend fun restoreBackup(uri: Uri) { + val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) + val json = JsonParser().parse(reader).asJsonObject - return Observable.just(Unit) - .map { - val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) - val json = JsonParser().parse(reader).asJsonObject + // Get parser version + val version = json.get(VERSION)?.asInt ?: 1 - // Get parser version - val version = json.get(VERSION)?.asInt ?: 1 + // Initialize manager + backupManager = BackupManager(this, version) - // Initialize manager - backupManager = BackupManager(this, version) + val mangasJson = json.get(MANGAS).asJsonArray - val mangasJson = json.get(MANGAS).asJsonArray + // +1 for categories + totalAmount = mangasJson.size() + 1 + trackingErrors.clear() + sourcesMissing.clear() + lincensedManga = 0 + errors.clear() + cancelled = 0 + // Restore categories + restoreCategories(json, backupManager) - restoreAmount = mangasJson.size() + 1 // +1 for categories - restoreProgress = 0 - errors.clear() - errorsMini.clear() + mangasJson.forEach { + restoreManga(it.asJsonObject, backupManager) + } - // Restore categories - json.get(CATEGORIES)?.let { - backupManager.restoreCategories(it.asJsonArray) - restoreProgress += 1 - showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) - } + notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS) - mangasJson + 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) + } + + + /**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(obj.get(MANGA)) + val chapters = backupManager.parser.fromJson>(obj.get(CHAPTERS) ?: JsonArray()) + val categories = backupManager.parser.fromJson>(obj.get(CATEGORIES) ?: JsonArray()) + val history = backupManager.parser.fromJson>(obj.get(HISTORY) ?: JsonArray()) + val tracks = backupManager.parser.fromJson>(obj.get(TRACK) ?: JsonArray()) + val source = backupManager.sourceManager.getOrStub(manga.source) + + try { + if (job?.isCancelled == false) { + showProgressNotification(restoreProgress, totalAmount, manga.title) + restoreProgress += 1 + } + else { + throw java.lang.Exception("Job was cancelled") + } + val dbManga = backupManager.getMangaFromDatabase(manga) + val dbMangaExists = dbManga != null + + 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) + } + + if (!dbMangaExists || !backupManager.restoreChaptersForManga(manga, chapters)) { + //manga gets chapters added + backupManager.restoreChapterFetch(source, manga, chapters) + } + // 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) } - .flatMap { Observable.from(it) } - .concatMap { - val obj = it.asJsonObject - val manga = backupManager.parser.fromJson(obj.get(MANGA)) - val chapters = backupManager.parser.fromJson>(obj.get(CHAPTERS) ?: JsonArray()) - val categories = backupManager.parser.fromJson>(obj.get(CATEGORIES) ?: JsonArray()) - val history = backupManager.parser.fromJson>(obj.get(HISTORY) ?: JsonArray()) - val tracks = backupManager.parser.fromJson>(obj.get(TRACK) ?: JsonArray()) + else if (e.message?.contains("licensed", true) == true) { + lincensedManga++ + } + errors.add("${manga.title} - ${cause?.message ?: e.message}") + return + } + errors.add("${manga.title} - ${e.message}") + } + } - val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks) - if (observable != null) { - observable - } else { - errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") - 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) - } - } - .toList() - .doOnNext { - 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) - - } - .doOnError { error -> - Timber.e(error) - writeErrorLog() - val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { - putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG) - putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) - } - sendLocalBroadcast(errorIntent) - } - .onErrorReturn { emptyList() } + /** + * [refreshes tracking information + * @param manga manga that needs updating. + * @param tracks list containing tracks from restore file. + */ + private fun trackingFetch(manga: Manga, tracks: List) { + 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) + } + } } /** @@ -291,186 +320,149 @@ class BackupRestoreService : Service() { val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) destFile.bufferedWriter().use { out -> - errors.forEach { (date, message) -> - out.write("[${sdf.format(date)}] $message\n") + errors.forEach { message -> + out.write("$message\n") } } return destFile } } catch (e: Exception) { - // Empty + Timber.e(e) } return File("") } /** - * Returns a manga restore observable - * - * @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 + * keep a partially constructed progress notification for resuse */ - private fun getMangaRestoreObservable(manga: Manga, chapters: List, - categories: List, history: List, - tracks: List): Observable? { - // Get source - val source = backupManager.sourceManager.getOrStub(manga.source) - val dbManga = backupManager.getMangaFromDatabase(manga) + private val progressNotification by lazy { + NotificationCompat.Builder(this, Notifications.CHANNEL_RESTORE) + .setContentTitle(getString(R.string.app_name)) + .setSmallIcon(R.drawable.ic_tachi) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setAutoCancel(false) + .setColor(ContextCompat.getColor(this, R.color.colorAccent)) + .addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) + } - return if (dbManga == null) { - // Manga not in database - 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) + /** + * Pending intent of action that cancels the library update + */ + private val cancelIntent by lazy { + NotificationReceiver.cancelRestorePendingBroadcast(this) + } + + + /** + * Shows the notification containing the currently updating manga and the progress. + * + * @param manga the manga that's being updated. + * @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 { + context.startForegroundService(intent) + } + } + } + + /** + * Stops the service. + * + * @param context the application context. + */ + fun stop(context: Context) { + context.stopService(Intent(context, BackupRestoreService::class.java)) } } - - /** - * [Observable] that fetches manga information - * - * @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, - categories: List, history: List, - tracks: List): Observable { - 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, - categories: List, history: List, - tracks: List): Observable { - - return Observable.just(backupManga) - .flatMap { manga -> - if (!backupManager.restoreChaptersForManga(manga, chapters)) { - chapterFetchObservable(source, manga, chapters) - .map { manga } - } else { - Observable.just(manga) - } - } - .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, history: List, tracks: List) { - // Restore categories - backupManager.restoreCategoriesForManga(manga, categories) - - // Restore history - backupManager.restoreHistoryForManga(history) - - // Restore tracking - backupManager.restoreTrackForManga(manga, tracks) - } - - /** - * [Observable] that fetches chapter information - * - * @param source source of manga - * @param manga manga that needs updating - * @return [Observable] that contains manga - */ - private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List): Observable, List>> { - return backupManager.restoreChapterFetchObservable(source, manga, chapters) - // 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): Observable { - 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) - } - -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 8baf4ecaa5..f2470be65a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -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, 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. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 816f59abab..43edd79714 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -100,6 +100,26 @@ class DownloadProvider(private val context: Context) { 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, manga: Manga, source: Source): List { + 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. * 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 index 298cc9356c..1852c688b5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -37,6 +37,10 @@ import eu.kanade.tachiyomi.util.isServiceRunning import eu.kanade.tachiyomi.util.notification import eu.kanade.tachiyomi.util.notificationManager 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.Subscription import rx.schedulers.Schedulers @@ -72,6 +76,9 @@ class LibraryUpdateService( */ private var subscription: Subscription? = null + var job: Job? = null + + /** * Pending intent of action that cancels the library update */ @@ -105,7 +112,8 @@ class LibraryUpdateService( enum class Target { CHAPTERS, // Manga chapters DETAILS, // Manga metadata - TRACKING // Tracking metadata + TRACKING, // Tracking metadata + CLEANUP // Clean up downloads } companion object { @@ -203,37 +211,43 @@ class LibraryUpdateService( * @return the start value of the command. */ 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 - ?: return Service.START_NOT_STICKY + ?: return START_NOT_STICKY // Unsubscribe from any previous subscription if needed. subscription?.unsubscribe() + val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() // Update favorite manga. Destroy service when completed or in case of an error. - subscription = Observable - .defer { - val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() - val mangaList = getMangaToUpdate(intent, target) - .sortedWith(rankingScheme[selectedScheme]) + val mangaList = getMangaToUpdate(intent, target) + .sortedWith(rankingScheme[selectedScheme]) - // Update either chapter list or manga details. - when (target) { - Target.CHAPTERS -> updateChapterList(mangaList) - Target.DETAILS -> updateDetails(mangaList) - Target.TRACKING -> updateTrackings(mangaList) - } + val handler = CoroutineExceptionHandler { _, exception -> + Timber.e(exception) + stopSelf(startId) + } + // 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) { + Target.CHAPTERS -> updateChapterList(mangaList) + Target.DETAILS -> updateDetails(mangaList) + else -> updateTrackings(mangaList) } - .subscribeOn(Schedulers.io()) - .subscribe({ - }, { + }.subscribeOn(Schedulers.io()).subscribe({}, { Timber.e(it) stopSelf(startId) }, { stopSelf(startId) }) - - return Service.START_REDELIVER_INTENT + } + return START_REDELIVER_INTENT } /** @@ -336,6 +350,15 @@ class LibraryUpdateService( .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) { // we need to get the chapters from the db so we have chapter ids val mangaChapters = db.getChapters(manga).executeAsBlocking() 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 b067006710..296dabbeb8 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 @@ -7,9 +7,11 @@ import android.content.BroadcastReceiver import android.content.ClipData import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Handler 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.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga @@ -31,6 +33,7 @@ import uy.kohesive.injekt.injectLazy import java.io.File import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID + /** * Global [BroadcastReceiver] that runs on UI thread * Pending Broadcasts should be made from here. @@ -65,6 +68,7 @@ class NotificationReceiver : BroadcastReceiver() { intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) // Cancel library update and dismiss notification ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context) + ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context) // Open reader activity ACTION_OPEN_CHAPTER -> { 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 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 { private const val NAME = "NotificationReceiver" @@ -197,9 +211,13 @@ class NotificationReceiver : BroadcastReceiver() { // Called to 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" + // Called to cancel restore + private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE" + + // Called to 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 * @@ -441,5 +472,18 @@ class NotificationReceiver : BroadcastReceiver() { } 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt index 03555be83b..25a91c3d64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -44,6 +44,12 @@ object Notifications { const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel" 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. * @@ -77,7 +83,10 @@ object Notifications { CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), NotificationManager.IMPORTANCE_DEFAULT - ) + ), NotificationChannel(CHANNEL_RESTORE, context.getString(R.string.channel_backup_restore), + NotificationManager.IMPORTANCE_LOW).apply { + setShowBadge(false) + } ) context.notificationManager.createNotificationChannels(channels) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index 8275728230..76301b6a98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -176,8 +176,7 @@ class ChaptersPresenter( var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) if (onlyUnread()) { observable = observable.filter { !it.read } - } - else if (onlyRead()) { + } else if (onlyRead()) { observable = observable.filter { it.read } } if (onlyDownloaded()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index 63ee2f2a36..03ba4a025a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -126,7 +126,7 @@ class MangaInfoPresenter( toggleFavorite() } - fun shareManga(cover:Bitmap) { + fun shareManga(cover: Bitmap) { val context = Injekt.get() val destDir = File(context.cacheDir, "shared_image") @@ -149,7 +149,7 @@ class MangaInfoPresenter( val destFile = File(directory, filename) val stream: OutputStream = FileOutputStream(destFile) - cover.compress(Bitmap.CompressFormat.JPEG,75,stream) + cover.compress(Bitmap.CompressFormat.JPEG, 75, stream) stream.flush() stream.close() return destFile diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 346cc1a971..0195f45635 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -69,6 +69,13 @@ class SettingsAdvancedController : SettingsController() { 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() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt index b0e4c7d655..b24500a047 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt @@ -288,7 +288,7 @@ class SettingsBackupController : SettingsController() { .onPositive { _, _ -> val context = applicationContext if (context != null) { - RestoringBackupDialog().showDialog(router, TAG_RESTORING_BACKUP_DIALOG) + activity?.toast(R.string.restoring_backup) BackupRestoreService.start(context, args.getParcelable(KEY_URI)!!) } } diff --git a/app/src/main/res/drawable/ic_error_grey.xml b/app/src/main/res/drawable/ic_error_grey.xml new file mode 100644 index 0000000000..be7b45024a --- /dev/null +++ b/app/src/main/res/drawable/ic_error_grey.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 9fbf3ccffc..f2b6152438 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -234,7 +234,7 @@ S\'ha completat la restauració No s\'ha pogut obrir el registre La restauració ha tardat %1$s. -\nS\'han produït $2$s errors. +\nS\'han produït %2$s errors. 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. S\'ha desat el fitxer a %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd06beb138..325023aad1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -306,13 +306,18 @@ Restoring backup\n%1$s source not found Backup created Restore completed + Restore error Could not open log - Restore took %1$s.\n%2$s errors found. - 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. + Restored %1$s. %2$s errors found + %1$d skipped + 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. File saved at %1$s What do you want to backup? Restoring backup Creating backup + Sources missing: %1$d + %1$d manga are now licensed and could not be restored + Not logged into %1$s Clear chapter cache @@ -330,6 +335,8 @@ Updates covers, genres, description and manga status information Refresh tracking metadata Updates status, score and last chapter read from the tracking services + Clean up downloaded chapters + Deletes orphaned,tmp, and read chapter folders for entire library for each Manga Version @@ -406,8 +413,6 @@ Delete downloaded chapters? %1$s copied to clipboard Source not installed: %1$s - Sources missing: %1$d - Not logged into %1$s Chapters @@ -534,6 +539,8 @@ Sync canceled Connection not available + View all errors + Select cover image Select backup file @@ -581,6 +588,7 @@ Library Downloader Extension Updates + Restore Backup Updating Library New Chapters diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt index 481ae4fe76..683d29e07f 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt +++ b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt @@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.source.SourceManager 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.fail import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -23,7 +26,6 @@ import org.mockito.Mockito.* import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config import rx.Observable -import rx.observers.TestSubscriber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar @@ -166,7 +168,7 @@ class BackupTest { assertThat(favoriteManga[0].viewer).isEqualTo(3) // Update json with all options enabled - mangaEntries.add(backupManager.backupMangaObject(manga,1)) + mangaEntries.add(backupManager.backupMangaObject(manga, 1)) // Change manga in database to default values val dbManga = getSingleManga("One Piece") @@ -178,7 +180,7 @@ class BackupTest { assertThat(favoriteManga[0].viewer).isEqualTo(0) // Restore local manga - backupManager.restoreMangaNoFetch(manga,dbManga) + backupManager.restoreMangaNoFetch(manga, dbManga) // Test if restore successful favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking() @@ -201,13 +203,16 @@ class BackupTest { // Restore manga with fetch observable val networkManga = getSingleManga("One Piece") 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) - val testSubscriber = TestSubscriber() - obs.subscribe(testSubscriber) + GlobalScope.launch { + try { + backupManager.restoreMangaFetch(source, jsonManga) + } catch (e: Exception) { + fail("Unexpected onError events") + } + } - testSubscriber.assertNoErrors() // Check if restore successful val dbCats = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking() @@ -231,7 +236,7 @@ class BackupTest { // Create restore list val chapters = ArrayList() - for (i in 1..8){ + for (i in 1..8) { val chapter = getSingleChapter("Chapter $i") chapter.read = true chapters.add(chapter) @@ -245,14 +250,16 @@ class BackupTest { // Create list val chaptersRemote = ArrayList() (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 - val obs = backupManager.restoreChapterFetchObservable(source, manga, restoredChapters) - val testSubscriber = TestSubscriber, List>>() - obs.subscribe(testSubscriber) - - testSubscriber.assertNoErrors() + GlobalScope.launch { + try { + backupManager.restoreChapterFetch(source, manga, restoredChapters) + } catch (e: Exception) { + fail("Unexpected onError events") + } + } val dbCats = backupManager.databaseHelper.getChapters(manga).executeAsBlocking() assertThat(dbCats).hasSize(10) @@ -263,7 +270,7 @@ class BackupTest { * Test to check if history restore works */ @Test - fun restoreHistoryForManga(){ + fun restoreHistoryForManga() { // Initialize json with version 2 initializeJsonTest(2) @@ -376,7 +383,7 @@ class BackupTest { return category } - fun clearDatabase(){ + fun clearDatabase() { db.deleteMangas().executeAsBlocking() db.deleteHistory().executeAsBlocking() }