Browse Source

More backup/restore code cleanup

arkon 2 years ago
parent
commit
9f0052eceb

+ 0 - 205
app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt

@@ -1,205 +0,0 @@
-package eu.kanade.tachiyomi.data.backup
-
-import android.content.Context
-import android.net.Uri
-import eu.kanade.data.DatabaseHandler
-import eu.kanade.data.toLong
-import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
-import eu.kanade.domain.chapter.model.toDbChapter
-import eu.kanade.domain.manga.interactor.GetFavorites
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.toDomainManga
-import eu.kanade.tachiyomi.data.database.models.toMangaInfo
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.source.model.toSChapter
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import data.Mangas as DbManga
-import eu.kanade.domain.manga.model.Manga as DomainManga
-
-abstract class AbstractBackupManager(protected val context: Context) {
-
-    protected val handler: DatabaseHandler = Injekt.get()
-
-    internal val sourceManager: SourceManager = Injekt.get()
-    internal val trackManager: TrackManager = Injekt.get()
-    protected val preferences: PreferencesHelper = Injekt.get()
-    private val getFavorites: GetFavorites = Injekt.get()
-    private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
-
-    abstract suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
-
-    /**
-     * Returns manga
-     *
-     * @return [Manga], null if not found
-     */
-    internal suspend fun getMangaFromDatabase(url: String, source: Long): DbManga? {
-        return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
-    }
-
-    /**
-     * Fetches chapter information.
-     *
-     * @param source source of manga
-     * @param manga manga that needs updating
-     * @param chapters list of chapters in the backup
-     * @return Updated manga chapters.
-     */
-    internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
-        val fetchedChapters = source.getChapterList(manga.toMangaInfo())
-            .map { it.toSChapter() }
-        val syncedChapters = syncChaptersWithSource.await(fetchedChapters, manga.toDomainManga()!!, source)
-        if (syncedChapters.first.isNotEmpty()) {
-            chapters.forEach { it.manga_id = manga.id }
-            updateChapters(chapters)
-        }
-        return syncedChapters.first.map { it.toDbChapter() } to syncedChapters.second.map { it.toDbChapter() }
-    }
-
-    /**
-     * Returns list containing manga from library
-     *
-     * @return [Manga] from library
-     */
-    protected suspend fun getFavoriteManga(): List<DomainManga> {
-        return getFavorites.await()
-    }
-
-    /**
-     * Inserts manga and returns id
-     *
-     * @return id of [Manga], null if not found
-     */
-    internal suspend fun insertManga(manga: Manga): Long {
-        return handler.awaitOne(true) {
-            mangasQueries.insert(
-                source = manga.source,
-                url = manga.url,
-                artist = manga.artist,
-                author = manga.author,
-                description = manga.description,
-                genre = manga.getGenres(),
-                title = manga.title,
-                status = manga.status.toLong(),
-                thumbnailUrl = manga.thumbnail_url,
-                favorite = manga.favorite,
-                lastUpdate = manga.last_update,
-                nextUpdate = 0L,
-                initialized = manga.initialized,
-                viewerFlags = manga.viewer_flags.toLong(),
-                chapterFlags = manga.chapter_flags.toLong(),
-                coverLastModified = manga.cover_last_modified,
-                dateAdded = manga.date_added,
-            )
-            mangasQueries.selectLastInsertedRowId()
-        }
-    }
-
-    internal suspend fun updateManga(manga: Manga): Long {
-        handler.await(true) {
-            mangasQueries.update(
-                source = manga.source,
-                url = manga.url,
-                artist = manga.artist,
-                author = manga.author,
-                description = manga.description,
-                genre = manga.genre,
-                title = manga.title,
-                status = manga.status.toLong(),
-                thumbnailUrl = manga.thumbnail_url,
-                favorite = manga.favorite.toLong(),
-                lastUpdate = manga.last_update,
-                initialized = manga.initialized.toLong(),
-                viewer = manga.viewer_flags.toLong(),
-                chapterFlags = manga.chapter_flags.toLong(),
-                coverLastModified = manga.cover_last_modified,
-                dateAdded = manga.date_added,
-                mangaId = manga.id!!,
-            )
-        }
-        return manga.id!!
-    }
-
-    /**
-     * Inserts list of chapters
-     */
-    protected suspend fun insertChapters(chapters: List<Chapter>) {
-        handler.await(true) {
-            chapters.forEach { chapter ->
-                chaptersQueries.insert(
-                    chapter.manga_id!!,
-                    chapter.url,
-                    chapter.name,
-                    chapter.scanlator,
-                    chapter.read,
-                    chapter.bookmark,
-                    chapter.last_page_read.toLong(),
-                    chapter.chapter_number,
-                    chapter.source_order.toLong(),
-                    chapter.date_fetch,
-                    chapter.date_upload,
-                )
-            }
-        }
-    }
-
-    /**
-     * Updates a list of chapters
-     */
-    protected suspend fun updateChapters(chapters: List<Chapter>) {
-        handler.await(true) {
-            chapters.forEach { chapter ->
-                chaptersQueries.update(
-                    chapter.manga_id!!,
-                    chapter.url,
-                    chapter.name,
-                    chapter.scanlator,
-                    chapter.read.toLong(),
-                    chapter.bookmark.toLong(),
-                    chapter.last_page_read.toLong(),
-                    chapter.chapter_number.toDouble(),
-                    chapter.source_order.toLong(),
-                    chapter.date_fetch,
-                    chapter.date_upload,
-                    chapter.id!!,
-                )
-            }
-        }
-    }
-
-    /**
-     * Updates a list of chapters with known database ids
-     */
-    protected suspend fun updateKnownChapters(chapters: List<Chapter>) {
-        handler.await(true) {
-            chapters.forEach { chapter ->
-                chaptersQueries.update(
-                    mangaId = null,
-                    url = null,
-                    name = null,
-                    scanlator = null,
-                    read = chapter.read.toLong(),
-                    bookmark = chapter.bookmark.toLong(),
-                    lastPageRead = chapter.last_page_read.toLong(),
-                    chapterNumber = null,
-                    sourceOrder = null,
-                    dateFetch = null,
-                    dateUpload = null,
-                    chapterId = chapter.id!!,
-                )
-            }
-        }
-    }
-
-    /**
-     * Return number of backups.
-     *
-     * @return number of backups selected by user
-     */
-    protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
-}

+ 0 - 153
app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupRestore.kt

@@ -1,153 +0,0 @@
-package eu.kanade.tachiyomi.data.backup
-
-import android.content.Context
-import android.net.Uri
-import eu.kanade.data.DatabaseHandler
-import eu.kanade.data.chapter.NoChaptersException
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.util.system.createFileInCacheDir
-import kotlinx.coroutines.Job
-import uy.kohesive.injekt.injectLazy
-import java.io.File
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-
-abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) {
-
-    protected val handler: DatabaseHandler by injectLazy()
-    protected val trackManager: TrackManager by injectLazy()
-
-    var job: Job? = null
-
-    protected lateinit var backupManager: T
-
-    protected var restoreAmount = 0
-    protected var restoreProgress = 0
-
-    /**
-     * Mapping of source ID to source name from backup data
-     */
-    protected var sourceMapping: Map<Long, String> = emptyMap()
-
-    protected val errors = mutableListOf<Pair<Date, String>>()
-
-    abstract suspend fun performRestore(uri: Uri): Boolean
-
-    suspend fun restoreBackup(uri: Uri): Boolean {
-        val startTime = System.currentTimeMillis()
-        restoreProgress = 0
-        errors.clear()
-
-        if (!performRestore(uri)) {
-            return false
-        }
-
-        val endTime = System.currentTimeMillis()
-        val time = endTime - startTime
-
-        val logFile = writeErrorLog()
-
-        notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
-        return true
-    }
-
-    /**
-     * Fetches chapter information.
-     *
-     * @param source source of manga
-     * @param manga manga that needs updating
-     * @return Updated manga chapters.
-     */
-    internal suspend fun updateChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
-        return try {
-            backupManager.restoreChapters(source, manga, chapters)
-        } catch (e: Exception) {
-            // If there's any error, return empty update and continue.
-            val errorMessage = if (e is NoChaptersException) {
-                context.getString(R.string.no_chapters_error)
-            } else {
-                e.message
-            }
-            errors.add(Date() to "${manga.title} - $errorMessage")
-            Pair(emptyList(), emptyList())
-        }
-    }
-
-    /**
-     * Refreshes tracking information.
-     *
-     * @param manga manga that needs updating.
-     * @param tracks list containing tracks from restore file.
-     */
-    internal suspend fun updateTracking(manga: Manga, tracks: List<Track>) {
-        tracks.forEach { track ->
-            val service = trackManager.getService(track.sync_id.toLong())
-            if (service != null && service.isLogged) {
-                try {
-                    val updatedTrack = service.refresh(track)
-                    handler.await {
-                        manga_syncQueries.insert(
-                            updatedTrack.manga_id,
-                            updatedTrack.sync_id.toLong(),
-                            updatedTrack.media_id,
-                            updatedTrack.library_id,
-                            updatedTrack.title,
-                            updatedTrack.last_chapter_read.toDouble(),
-                            updatedTrack.total_chapters.toLong(),
-                            updatedTrack.status.toLong(),
-                            updatedTrack.score,
-                            updatedTrack.tracking_url,
-                            updatedTrack.started_reading_date,
-                            updatedTrack.finished_reading_date,
-                        )
-                    }
-                } catch (e: Exception) {
-                    errors.add(Date() to "${manga.title} - ${e.message}")
-                }
-            } else {
-                val serviceName = service?.nameRes()?.let { context.getString(it) }
-                errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}")
-            }
-        }
-    }
-
-    /**
-     * Called to update dialog in [BackupConst]
-     *
-     * @param progress restore progress
-     * @param amount total restoreAmount of manga
-     * @param title title of restored manga
-     */
-    internal fun showRestoreProgress(
-        progress: Int,
-        amount: Int,
-        title: String,
-    ) {
-        notifier.showRestoreProgress(title, progress, amount)
-    }
-
-    internal fun writeErrorLog(): File {
-        try {
-            if (errors.isNotEmpty()) {
-                val file = context.createFileInCacheDir("tachiyomi_restore.txt")
-                val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
-
-                file.bufferedWriter().use { out ->
-                    errors.forEach { (date, message) ->
-                        out.write("[${sdf.format(date)}] $message\n")
-                    }
-                }
-                return file
-            }
-        } catch (e: Exception) {
-            // Empty
-        }
-        return File("")
-    }
-}

+ 165 - 20
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt

@@ -5,9 +5,12 @@ import android.net.Uri
 import com.hippo.unifile.UniFile
 import data.Manga_sync
 import data.Mangas
-import eu.kanade.data.category.categoryMapper
+import eu.kanade.data.DatabaseHandler
+import eu.kanade.data.toLong
+import eu.kanade.domain.category.interactor.GetCategories
 import eu.kanade.domain.category.model.Category
 import eu.kanade.domain.history.model.HistoryUpdate
+import eu.kanade.domain.manga.interactor.GetFavorites
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
 import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
@@ -29,35 +32,43 @@ import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.util.system.logcat
 import kotlinx.serialization.protobuf.ProtoBuf
 import logcat.LogPriority
 import okio.buffer
 import okio.gzip
 import okio.sink
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import java.io.FileOutputStream
 import java.util.Date
 import kotlin.math.max
 import eu.kanade.domain.manga.model.Manga as DomainManga
 
-class BackupManager(context: Context) : AbstractBackupManager(context) {
+class BackupManager(
+    private val context: Context,
+) {
 
-    val parser = ProtoBuf
+    private val handler: DatabaseHandler = Injekt.get()
+    private val sourceManager: SourceManager = Injekt.get()
+    private val preferences: PreferencesHelper = Injekt.get()
+    private val getCategories: GetCategories = Injekt.get()
+    private val getFavorites: GetFavorites = Injekt.get()
+
+    internal val parser = ProtoBuf
 
     /**
-     * Create backup Json file from database
+     * Create backup file from database
      *
      * @param uri path of Uri
      * @param isAutoBackup backup called from scheduled backup job
      */
     @Suppress("BlockingMethodInNonBlockingContext")
-    override suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
-        // Create root object
-        var backup: Backup? = null
-
-        val databaseManga = getFavoriteManga()
-
-        backup = Backup(
+    suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
+        val databaseManga = getFavorites.await()
+        val backup = Backup(
             backupMangas(databaseManga, flags),
             backupCategories(flags),
             emptyList(),
@@ -73,7 +84,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
                     dir = dir.createDirectory("automatic")
 
                     // Delete older backups
-                    val numberOfBackups = numberOfBackups()
+                    val numberOfBackups = preferences.numberOfBackups().get()
                     val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
                     dir.listFiles { _, filename -> backupRegex.matches(filename) }
                         .orEmpty()
@@ -93,7 +104,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
                 throw IllegalStateException("Failed to get handle on file")
             }
 
-            val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
+            val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
             if (byteArray.isEmpty()) {
                 throw IllegalStateException(context.getString(R.string.empty_backup_error))
             }
@@ -133,7 +144,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
     private suspend fun backupCategories(options: Int): List<BackupCategory> {
         // Check if user wants category information in backup
         return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
-            handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
+            getCategories.await()
                 .filterNot(Category::isSystemCategory)
                 .map(backupCategoryMapper)
         } else {
@@ -170,7 +181,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
         // Check if user wants category information in backup
         if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
             // Backup categories for this manga
-            val categoriesForManga = handler.awaitList { categoriesQueries.getCategoriesByMangaId(manga.id) }
+            val categoriesForManga = getCategories.await(manga.id)
             if (categoriesForManga.isNotEmpty()) {
                 mangaObject.categories = categoriesForManga.map { it.order }
             }
@@ -201,7 +212,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
         return mangaObject
     }
 
-    suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas) {
+    internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas) {
         manga.id = dbManga._id
         manga.copyFrom(dbManga)
         updateManga(manga)
@@ -213,7 +224,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
      * @param manga manga that needs updating
      * @return Updated manga info.
      */
-    suspend fun restoreNewManga(manga: Manga): Manga {
+    internal suspend fun restoreNewManga(manga: Manga): Manga {
         return manga.also {
             it.initialized = it.description != null
             it.id = insertManga(it)
@@ -227,7 +238,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
      */
     internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
         // Get categories from file and from db
-        val dbCategories = handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
+        val dbCategories = getCategories.await()
 
         val categories = backupCategories.map {
             var category = it.getCategory()
@@ -267,7 +278,7 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
      * @param categories the categories to restore.
      */
     internal suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
-        val dbCategories = handler.awaitList { categoriesQueries.getCategories() }
+        val dbCategories = getCategories.await()
         val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
 
         categories.forEach { backupCategoryOrder ->
@@ -353,7 +364,6 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
         tracks.map { it.manga_id = manga.id!! }
 
         // Get tracks from database
-
         val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id!!) }
         val toUpdate = mutableListOf<Manga_sync>()
         val toInsert = mutableListOf<Track>()
@@ -452,4 +462,139 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
         newChapters[true]?.let { updateKnownChapters(it) }
         newChapters[false]?.let { insertChapters(it) }
     }
+
+    /**
+     * Returns manga
+     *
+     * @return [Manga], null if not found
+     */
+    internal suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
+        return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
+    }
+
+    /**
+     * Inserts manga and returns id
+     *
+     * @return id of [Manga], null if not found
+     */
+    private suspend fun insertManga(manga: Manga): Long {
+        return handler.awaitOne(true) {
+            mangasQueries.insert(
+                source = manga.source,
+                url = manga.url,
+                artist = manga.artist,
+                author = manga.author,
+                description = manga.description,
+                genre = manga.getGenres(),
+                title = manga.title,
+                status = manga.status.toLong(),
+                thumbnailUrl = manga.thumbnail_url,
+                favorite = manga.favorite,
+                lastUpdate = manga.last_update,
+                nextUpdate = 0L,
+                initialized = manga.initialized,
+                viewerFlags = manga.viewer_flags.toLong(),
+                chapterFlags = manga.chapter_flags.toLong(),
+                coverLastModified = manga.cover_last_modified,
+                dateAdded = manga.date_added,
+            )
+            mangasQueries.selectLastInsertedRowId()
+        }
+    }
+
+    private suspend fun updateManga(manga: Manga): Long {
+        handler.await(true) {
+            mangasQueries.update(
+                source = manga.source,
+                url = manga.url,
+                artist = manga.artist,
+                author = manga.author,
+                description = manga.description,
+                genre = manga.genre,
+                title = manga.title,
+                status = manga.status.toLong(),
+                thumbnailUrl = manga.thumbnail_url,
+                favorite = manga.favorite.toLong(),
+                lastUpdate = manga.last_update,
+                initialized = manga.initialized.toLong(),
+                viewer = manga.viewer_flags.toLong(),
+                chapterFlags = manga.chapter_flags.toLong(),
+                coverLastModified = manga.cover_last_modified,
+                dateAdded = manga.date_added,
+                mangaId = manga.id!!,
+            )
+        }
+        return manga.id!!
+    }
+
+    /**
+     * Inserts list of chapters
+     */
+    private suspend fun insertChapters(chapters: List<Chapter>) {
+        handler.await(true) {
+            chapters.forEach { chapter ->
+                chaptersQueries.insert(
+                    chapter.manga_id!!,
+                    chapter.url,
+                    chapter.name,
+                    chapter.scanlator,
+                    chapter.read,
+                    chapter.bookmark,
+                    chapter.last_page_read.toLong(),
+                    chapter.chapter_number,
+                    chapter.source_order.toLong(),
+                    chapter.date_fetch,
+                    chapter.date_upload,
+                )
+            }
+        }
+    }
+
+    /**
+     * Updates a list of chapters
+     */
+    private suspend fun updateChapters(chapters: List<Chapter>) {
+        handler.await(true) {
+            chapters.forEach { chapter ->
+                chaptersQueries.update(
+                    chapter.manga_id!!,
+                    chapter.url,
+                    chapter.name,
+                    chapter.scanlator,
+                    chapter.read.toLong(),
+                    chapter.bookmark.toLong(),
+                    chapter.last_page_read.toLong(),
+                    chapter.chapter_number.toDouble(),
+                    chapter.source_order.toLong(),
+                    chapter.date_fetch,
+                    chapter.date_upload,
+                    chapter.id!!,
+                )
+            }
+        }
+    }
+
+    /**
+     * Updates a list of chapters with known database ids
+     */
+    private suspend fun updateKnownChapters(chapters: List<Chapter>) {
+        handler.await(true) {
+            chapters.forEach { chapter ->
+                chaptersQueries.update(
+                    mangaId = null,
+                    url = null,
+                    name = null,
+                    scanlator = null,
+                    read = chapter.read.toLong(),
+                    bookmark = chapter.bookmark.toLong(),
+                    lastPageRead = chapter.last_page_read.toLong(),
+                    chapterNumber = null,
+                    sourceOrder = null,
+                    dateFetch = null,
+                    dateUpload = null,
+                    chapterId = chapter.id!!,
+                )
+            }
+        }
+    }
 }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt

@@ -69,7 +69,7 @@ class BackupRestoreService : Service() {
     private lateinit var wakeLock: PowerManager.WakeLock
 
     private lateinit var ioScope: CoroutineScope
-    private var restorer: AbstractBackupRestore<*>? = null
+    private var restorer: BackupRestorer? = null
     private lateinit var notifier: BackupNotifier
 
     override fun onCreate() {

+ 72 - 4
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt

@@ -11,17 +11,74 @@ import eu.kanade.tachiyomi.data.backup.models.BackupSource
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.util.system.createFileInCacheDir
+import kotlinx.coroutines.Job
 import okio.buffer
 import okio.gzip
 import okio.source
+import java.io.File
+import java.text.SimpleDateFormat
 import java.util.Date
+import java.util.Locale
 
-class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<BackupManager>(context, notifier) {
+class BackupRestorer(
+    private val context: Context,
+    private val notifier: BackupNotifier,
+) {
 
-    @Suppress("BlockingMethodInNonBlockingContext")
-    override suspend fun performRestore(uri: Uri): Boolean {
-        backupManager = BackupManager(context)
+    var job: Job? = null
+
+    private var backupManager = BackupManager(context)
+
+    private var restoreAmount = 0
+    private var restoreProgress = 0
+
+    /**
+     * Mapping of source ID to source name from backup data
+     */
+    private var sourceMapping: Map<Long, String> = emptyMap()
+
+    private val errors = mutableListOf<Pair<Date, String>>()
 
+    suspend fun restoreBackup(uri: Uri): Boolean {
+        val startTime = System.currentTimeMillis()
+        restoreProgress = 0
+        errors.clear()
+
+        if (!performRestore(uri)) {
+            return false
+        }
+
+        val endTime = System.currentTimeMillis()
+        val time = endTime - startTime
+
+        val logFile = writeErrorLog()
+
+        notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
+        return true
+    }
+
+    fun writeErrorLog(): File {
+        try {
+            if (errors.isNotEmpty()) {
+                val file = context.createFileInCacheDir("tachiyomi_restore.txt")
+                val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
+
+                file.bufferedWriter().use { out ->
+                    errors.forEach { (date, message) ->
+                        out.write("[${sdf.format(date)}] $message\n")
+                    }
+                }
+                return file
+            }
+        } catch (e: Exception) {
+            // Empty
+        }
+        return File("")
+    }
+
+    @Suppress("BlockingMethodInNonBlockingContext")
+    private suspend fun performRestore(uri: Uri): Boolean {
         val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
         val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
 
@@ -125,4 +182,15 @@ class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBacku
         backupManager.restoreHistory(history)
         backupManager.restoreTracking(manga, tracks)
     }
+
+    /**
+     * 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) {
+        notifier.showRestoreProgress(title, progress, amount)
+    }
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt

@@ -68,7 +68,7 @@ open class BrowseSourcePresenter(
     private val sourceId: Long,
     searchQuery: String? = null,
     private val sourceManager: SourceManager = Injekt.get(),
-    private val prefs: PreferencesHelper = Injekt.get(),
+    private val preferences: PreferencesHelper = Injekt.get(),
     private val coverCache: CoverCache = Injekt.get(),
     private val getManga: GetManga = Injekt.get(),
     private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
@@ -153,7 +153,7 @@ open class BrowseSourcePresenter(
         pager = createPager(query, filters)
 
         val sourceId = source.id
-        val sourceDisplayMode = prefs.sourceDisplayMode()
+        val sourceDisplayMode = preferences.sourceDisplayMode()
 
         pagerJob?.cancel()
         pagerJob = presenterScope.launchIO {

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/more/MorePresenter.kt

@@ -18,11 +18,11 @@ import uy.kohesive.injekt.api.get
 
 class MorePresenter(
     private val downloadManager: DownloadManager = Injekt.get(),
-    preferencesHelper: PreferencesHelper = Injekt.get(),
+    preferences: PreferencesHelper = Injekt.get(),
 ) : BasePresenter<MoreController>() {
 
-    val downloadedOnly = preferencesHelper.downloadedOnly().asState()
-    val incognitoMode = preferencesHelper.incognitoMode().asState()
+    val downloadedOnly = preferences.downloadedOnly().asState()
+    val incognitoMode = preferences.incognitoMode().asState()
 
     private var _state: MutableStateFlow<DownloadQueueState> = MutableStateFlow(DownloadQueueState.Stopped)
     val downloadQueueState: StateFlow<DownloadQueueState> = _state.asStateFlow()

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt

@@ -50,17 +50,17 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int {
     return coverCache.deleteFromCache(this, true)
 }
 
-fun DomainManga.shouldDownloadNewChapters(dbCategories: List<Long>, prefs: PreferencesHelper): Boolean {
+fun DomainManga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: PreferencesHelper): Boolean {
     if (!favorite) return false
 
     val categories = dbCategories.ifEmpty { listOf(0L) }
 
     // Boolean to determine if user wants to automatically download new chapters.
-    val downloadNewChapter = prefs.downloadNewChapter().get()
+    val downloadNewChapter = preferences.downloadNewChapter().get()
     if (!downloadNewChapter) return false
 
-    val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toLong() }
-    val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toLong() }
+    val includedCategories = preferences.downloadNewChapterCategories().get().map { it.toLong() }
+    val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get().map { it.toLong() }
 
     // Default: Download from all categories
     if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true

+ 20 - 20
app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSettingsHelper.kt

@@ -10,7 +10,7 @@ import uy.kohesive.injekt.injectLazy
 
 object ChapterSettingsHelper {
 
-    private val prefs: PreferencesHelper by injectLazy()
+    private val preferences: PreferencesHelper by injectLazy()
     private val getFavorites: GetFavorites by injectLazy()
     private val setMangaChapterFlags: SetMangaChapterFlags by injectLazy()
 
@@ -18,7 +18,7 @@ object ChapterSettingsHelper {
      * Updates the global Chapter Settings in Preferences.
      */
     fun setGlobalSettings(manga: Manga) {
-        prefs.setChapterSettingsDefault(manga.toDbManga())
+        preferences.setChapterSettingsDefault(manga.toDbManga())
     }
 
     /**
@@ -28,12 +28,12 @@ object ChapterSettingsHelper {
         launchIO {
             setMangaChapterFlags.awaitSetAllFlags(
                 mangaId = manga.id,
-                unreadFilter = prefs.filterChapterByRead().toLong(),
-                downloadedFilter = prefs.filterChapterByDownloaded().toLong(),
-                bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(),
-                sortingMode = prefs.sortChapterBySourceOrNumber().toLong(),
-                sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(),
-                displayMode = prefs.displayChapterByNameOrNumber().toLong(),
+                unreadFilter = preferences.filterChapterByRead().toLong(),
+                downloadedFilter = preferences.filterChapterByDownloaded().toLong(),
+                bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(),
+                sortingMode = preferences.sortChapterBySourceOrNumber().toLong(),
+                sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(),
+                displayMode = preferences.displayChapterByNameOrNumber().toLong(),
             )
         }
     }
@@ -41,12 +41,12 @@ object ChapterSettingsHelper {
     suspend fun applySettingDefaults(mangaId: Long) {
         setMangaChapterFlags.awaitSetAllFlags(
             mangaId = mangaId,
-            unreadFilter = prefs.filterChapterByRead().toLong(),
-            downloadedFilter = prefs.filterChapterByDownloaded().toLong(),
-            bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(),
-            sortingMode = prefs.sortChapterBySourceOrNumber().toLong(),
-            sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(),
-            displayMode = prefs.displayChapterByNameOrNumber().toLong(),
+            unreadFilter = preferences.filterChapterByRead().toLong(),
+            downloadedFilter = preferences.filterChapterByDownloaded().toLong(),
+            bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(),
+            sortingMode = preferences.sortChapterBySourceOrNumber().toLong(),
+            sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(),
+            displayMode = preferences.displayChapterByNameOrNumber().toLong(),
         )
     }
 
@@ -59,12 +59,12 @@ object ChapterSettingsHelper {
                 .map { manga ->
                     setMangaChapterFlags.awaitSetAllFlags(
                         mangaId = manga.id,
-                        unreadFilter = prefs.filterChapterByRead().toLong(),
-                        downloadedFilter = prefs.filterChapterByDownloaded().toLong(),
-                        bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(),
-                        sortingMode = prefs.sortChapterBySourceOrNumber().toLong(),
-                        sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(),
-                        displayMode = prefs.displayChapterByNameOrNumber().toLong(),
+                        unreadFilter = preferences.filterChapterByRead().toLong(),
+                        downloadedFilter = preferences.filterChapterByDownloaded().toLong(),
+                        bookmarkedFilter = preferences.filterChapterByBookmarked().toLong(),
+                        sortingMode = preferences.sortChapterBySourceOrNumber().toLong(),
+                        sortingDirection = preferences.sortChapterByAscendingOrDescending().toLong(),
+                        displayMode = preferences.displayChapterByNameOrNumber().toLong(),
                     )
                 }
         }

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt

@@ -319,8 +319,8 @@ fun Context.isNightMode(): Boolean {
  * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898
  */
 fun Context.createReaderThemeContext(): Context {
-    val prefs = Injekt.get<PreferencesHelper>()
-    val isDarkBackground = when (prefs.readerTheme().get()) {
+    val preferences = Injekt.get<PreferencesHelper>()
+    val isDarkBackground = when (preferences.readerTheme().get()) {
         1, 2 -> true // Black, Gray
         3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
         else -> false // White
@@ -333,7 +333,7 @@ fun Context.createReaderThemeContext(): Context {
 
         val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi)
         wrappedContext.applyOverrideConfiguration(overrideConf)
-        ThemingDelegate.getThemeResIds(prefs.appTheme().get(), prefs.themeDarkAmoled().get())
+        ThemingDelegate.getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get())
             .forEach { wrappedContext.theme.applyStyle(it, true) }
         return wrappedContext
     }