Kaynağa Gözat

Move backup restoring functions from BackupManager to BackupRestorer

arkon 1 yıl önce
ebeveyn
işleme
6dab94a937

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

@@ -49,7 +49,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
         }
 
         return try {
-            val location = BackupManager(context).createBackup(uri, flags, isAutoBackup)
+            val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
             if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
             Result.success()
         } catch (e: Exception) {

+ 268 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt

@@ -0,0 +1,268 @@
+package eu.kanade.tachiyomi.data.backup
+
+import android.Manifest
+import android.content.Context
+import android.net.Uri
+import com.hippo.unifile.UniFile
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
+import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
+import eu.kanade.tachiyomi.data.backup.models.Backup
+import eu.kanade.tachiyomi.data.backup.models.BackupCategory
+import eu.kanade.tachiyomi.data.backup.models.BackupHistory
+import eu.kanade.tachiyomi.data.backup.models.BackupManga
+import eu.kanade.tachiyomi.data.backup.models.BackupPreference
+import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
+import eu.kanade.tachiyomi.data.backup.models.BackupSource
+import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
+import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
+import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
+import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
+import eu.kanade.tachiyomi.source.ConfigurableSource
+import eu.kanade.tachiyomi.source.preferenceKey
+import eu.kanade.tachiyomi.source.sourcePreferences
+import eu.kanade.tachiyomi.util.system.hasPermission
+import kotlinx.serialization.protobuf.ProtoBuf
+import logcat.LogPriority
+import okio.buffer
+import okio.gzip
+import okio.sink
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.util.system.logcat
+import tachiyomi.data.DatabaseHandler
+import tachiyomi.domain.backup.service.BackupPreferences
+import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.category.model.Category
+import tachiyomi.domain.history.interactor.GetHistory
+import tachiyomi.domain.manga.interactor.GetFavorites
+import tachiyomi.domain.manga.model.Manga
+import tachiyomi.domain.source.service.SourceManager
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.FileOutputStream
+
+class BackupCreator(
+    private val context: Context,
+) {
+
+    private val handler: DatabaseHandler = Injekt.get()
+    private val sourceManager: SourceManager = Injekt.get()
+    private val backupPreferences: BackupPreferences = Injekt.get()
+    private val getCategories: GetCategories = Injekt.get()
+    private val getFavorites: GetFavorites = Injekt.get()
+    private val getHistory: GetHistory = Injekt.get()
+    private val preferenceStore: PreferenceStore = Injekt.get()
+
+    internal val parser = ProtoBuf
+
+    /**
+     * Create backup file.
+     *
+     * @param uri path of Uri
+     * @param isAutoBackup backup called from scheduled backup job
+     */
+    suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
+        if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+            throw IllegalStateException(context.getString(R.string.missing_storage_permission))
+        }
+
+        val databaseManga = getFavorites.await()
+        val backup = Backup(
+            backupMangas(databaseManga, flags),
+            backupCategories(flags),
+            emptyList(),
+            prepExtensionInfoForSync(databaseManga),
+            backupAppPreferences(flags),
+            backupSourcePreferences(flags),
+        )
+
+        var file: UniFile? = null
+        try {
+            file = (
+                if (isAutoBackup) {
+                    // Get dir of file and create
+                    var dir = UniFile.fromUri(context, uri)
+                    dir = dir.createDirectory("automatic")
+
+                    // Delete older backups
+                    val numberOfBackups = backupPreferences.numberOfBackups().get()
+                    dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
+                        .orEmpty()
+                        .sortedByDescending { it.name }
+                        .drop(numberOfBackups - 1)
+                        .forEach { it.delete() }
+
+                    // Create new file to place backup
+                    dir.createFile(Backup.getFilename())
+                } else {
+                    UniFile.fromUri(context, uri)
+                }
+                )
+                ?: throw Exception(context.getString(R.string.create_backup_file_error))
+
+            if (!file.isFile) {
+                throw IllegalStateException("Failed to get handle on a backup file")
+            }
+
+            val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
+            if (byteArray.isEmpty()) {
+                throw IllegalStateException(context.getString(R.string.empty_backup_error))
+            }
+
+            file.openOutputStream().also {
+                // Force overwrite old file
+                (it as? FileOutputStream)?.channel?.truncate(0)
+            }.sink().gzip().buffer().use { it.write(byteArray) }
+            val fileUri = file.uri
+
+            // Make sure it's a valid backup file
+            BackupFileValidator().validate(context, fileUri)
+
+            return fileUri.toString()
+        } catch (e: Exception) {
+            logcat(LogPriority.ERROR, e)
+            file?.delete()
+            throw e
+        }
+    }
+
+    private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
+        return mangas
+            .asSequence()
+            .map(Manga::source)
+            .distinct()
+            .map(sourceManager::getOrStub)
+            .map(BackupSource::copyFrom)
+            .toList()
+    }
+
+    /**
+     * Backup the categories of library
+     *
+     * @return list of [BackupCategory] to be backed up
+     */
+    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) {
+            getCategories.await()
+                .filterNot(Category::isSystemCategory)
+                .map(backupCategoryMapper)
+        } else {
+            emptyList()
+        }
+    }
+
+    private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
+        return mangas.map {
+            backupManga(it, flags)
+        }
+    }
+
+    /**
+     * Convert a manga to Json
+     *
+     * @param manga manga that gets converted
+     * @param options options for the backup
+     * @return [BackupManga] containing manga in a serializable form
+     */
+    private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
+        // Entry for this manga
+        val mangaObject = BackupManga.copyFrom(manga)
+
+        // Check if user wants chapter information in backup
+        if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
+            // Backup all the chapters
+            val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
+            if (chapters.isNotEmpty()) {
+                mangaObject.chapters = chapters
+            }
+        }
+
+        // Check if user wants category information in backup
+        if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
+            // Backup categories for this manga
+            val categoriesForManga = getCategories.await(manga.id)
+            if (categoriesForManga.isNotEmpty()) {
+                mangaObject.categories = categoriesForManga.map { it.order }
+            }
+        }
+
+        // Check if user wants track information in backup
+        if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
+            val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
+            if (tracks.isNotEmpty()) {
+                mangaObject.tracking = tracks
+            }
+        }
+
+        // Check if user wants history information in backup
+        if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
+            val historyByMangaId = getHistory.await(manga.id)
+            if (historyByMangaId.isNotEmpty()) {
+                val history = historyByMangaId.map { history ->
+                    val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
+                    BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
+                }
+                if (history.isNotEmpty()) {
+                    mangaObject.history = history
+                }
+            }
+        }
+
+        return mangaObject
+    }
+
+    private fun backupAppPreferences(flags: Int): List<BackupPreference> {
+        if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
+
+        return preferenceStore.getAll().toBackupPreferences()
+    }
+
+    private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
+        if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
+
+        return sourceManager.getOnlineSources()
+            .filterIsInstance<ConfigurableSource>()
+            .map {
+                BackupSourcePreferences(
+                    it.preferenceKey(),
+                    it.sourcePreferences().all.toBackupPreferences(),
+                )
+            }
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
+        return this.filterKeys { !Preference.isPrivate(it) }
+            .mapNotNull { (key, value) ->
+                when (value) {
+                    is Int -> BackupPreference(key, IntPreferenceValue(value))
+                    is Long -> BackupPreference(key, LongPreferenceValue(value))
+                    is Float -> BackupPreference(key, FloatPreferenceValue(value))
+                    is String -> BackupPreference(key, StringPreferenceValue(value))
+                    is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
+                    is Set<*> -> (value as? Set<String>)?.let {
+                        BackupPreference(key, StringSetPreferenceValue(it))
+                    }
+                    else -> null
+                }
+            }
+    }
+}

+ 0 - 646
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt

@@ -1,646 +0,0 @@
-package eu.kanade.tachiyomi.data.backup
-
-import android.Manifest
-import android.content.Context
-import android.net.Uri
-import com.hippo.unifile.UniFile
-import eu.kanade.domain.chapter.model.copyFrom
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
-import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
-import eu.kanade.tachiyomi.data.backup.models.Backup
-import eu.kanade.tachiyomi.data.backup.models.BackupCategory
-import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
-import eu.kanade.tachiyomi.data.backup.models.BackupHistory
-import eu.kanade.tachiyomi.data.backup.models.BackupManga
-import eu.kanade.tachiyomi.data.backup.models.BackupPreference
-import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
-import eu.kanade.tachiyomi.data.backup.models.BackupSource
-import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
-import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
-import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
-import eu.kanade.tachiyomi.source.ConfigurableSource
-import eu.kanade.tachiyomi.source.model.copyFrom
-import eu.kanade.tachiyomi.source.preferenceKey
-import eu.kanade.tachiyomi.source.sourcePreferences
-import eu.kanade.tachiyomi.util.system.hasPermission
-import kotlinx.serialization.protobuf.ProtoBuf
-import logcat.LogPriority
-import okio.buffer
-import okio.gzip
-import okio.sink
-import tachiyomi.core.preference.Preference
-import tachiyomi.core.preference.PreferenceStore
-import tachiyomi.core.util.system.logcat
-import tachiyomi.data.DatabaseHandler
-import tachiyomi.data.Manga_sync
-import tachiyomi.data.Mangas
-import tachiyomi.data.UpdateStrategyColumnAdapter
-import tachiyomi.domain.backup.service.BackupPreferences
-import tachiyomi.domain.category.interactor.GetCategories
-import tachiyomi.domain.category.model.Category
-import tachiyomi.domain.history.interactor.GetHistory
-import tachiyomi.domain.history.model.HistoryUpdate
-import tachiyomi.domain.library.service.LibraryPreferences
-import tachiyomi.domain.manga.interactor.GetFavorites
-import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.source.service.SourceManager
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.io.FileOutputStream
-import java.util.Date
-import kotlin.math.max
-
-class BackupManager(
-    private val context: Context,
-) {
-
-    private val handler: DatabaseHandler = Injekt.get()
-    private val sourceManager: SourceManager = Injekt.get()
-    private val backupPreferences: BackupPreferences = Injekt.get()
-    private val libraryPreferences: LibraryPreferences = Injekt.get()
-    private val getCategories: GetCategories = Injekt.get()
-    private val getFavorites: GetFavorites = Injekt.get()
-    private val getHistory: GetHistory = Injekt.get()
-    private val preferenceStore: PreferenceStore = Injekt.get()
-
-    internal val parser = ProtoBuf
-
-    /**
-     * Create backup file from database
-     *
-     * @param uri path of Uri
-     * @param isAutoBackup backup called from scheduled backup job
-     */
-    suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
-        if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
-            throw IllegalStateException(context.getString(R.string.missing_storage_permission))
-        }
-
-        val databaseManga = getFavorites.await()
-        val backup = Backup(
-            backupMangas(databaseManga, flags),
-            backupCategories(flags),
-            emptyList(),
-            prepExtensionInfoForSync(databaseManga),
-            backupAppPreferences(flags),
-            backupSourcePreferences(flags),
-        )
-
-        var file: UniFile? = null
-        try {
-            file = (
-                if (isAutoBackup) {
-                    // Get dir of file and create
-                    var dir = UniFile.fromUri(context, uri)
-                    dir = dir.createDirectory("automatic")
-
-                    // Delete older backups
-                    val numberOfBackups = backupPreferences.numberOfBackups().get()
-                    dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
-                        .orEmpty()
-                        .sortedByDescending { it.name }
-                        .drop(numberOfBackups - 1)
-                        .forEach { it.delete() }
-
-                    // Create new file to place backup
-                    dir.createFile(Backup.getFilename())
-                } else {
-                    UniFile.fromUri(context, uri)
-                }
-                )
-                ?: throw Exception(context.getString(R.string.create_backup_file_error))
-
-            if (!file.isFile) {
-                throw IllegalStateException("Failed to get handle on a backup file")
-            }
-
-            val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
-            if (byteArray.isEmpty()) {
-                throw IllegalStateException(context.getString(R.string.empty_backup_error))
-            }
-
-            file.openOutputStream().also {
-                // Force overwrite old file
-                (it as? FileOutputStream)?.channel?.truncate(0)
-            }.sink().gzip().buffer().use { it.write(byteArray) }
-            val fileUri = file.uri
-
-            // Make sure it's a valid backup file
-            BackupFileValidator().validate(context, fileUri)
-
-            return fileUri.toString()
-        } catch (e: Exception) {
-            logcat(LogPriority.ERROR, e)
-            file?.delete()
-            throw e
-        }
-    }
-
-    private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
-        return mangas
-            .asSequence()
-            .map(Manga::source)
-            .distinct()
-            .map(sourceManager::getOrStub)
-            .map(BackupSource::copyFrom)
-            .toList()
-    }
-
-    /**
-     * Backup the categories of library
-     *
-     * @return list of [BackupCategory] to be backed up
-     */
-    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) {
-            getCategories.await()
-                .filterNot(Category::isSystemCategory)
-                .map(backupCategoryMapper)
-        } else {
-            emptyList()
-        }
-    }
-
-    private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
-        return mangas.map {
-            backupManga(it, flags)
-        }
-    }
-
-    /**
-     * Convert a manga to Json
-     *
-     * @param manga manga that gets converted
-     * @param options options for the backup
-     * @return [BackupManga] containing manga in a serializable form
-     */
-    private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
-        // Entry for this manga
-        val mangaObject = BackupManga.copyFrom(manga)
-
-        // Check if user wants chapter information in backup
-        if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
-            // Backup all the chapters
-            val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
-            if (chapters.isNotEmpty()) {
-                mangaObject.chapters = chapters
-            }
-        }
-
-        // Check if user wants category information in backup
-        if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
-            // Backup categories for this manga
-            val categoriesForManga = getCategories.await(manga.id)
-            if (categoriesForManga.isNotEmpty()) {
-                mangaObject.categories = categoriesForManga.map { it.order }
-            }
-        }
-
-        // Check if user wants track information in backup
-        if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
-            val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
-            if (tracks.isNotEmpty()) {
-                mangaObject.tracking = tracks
-            }
-        }
-
-        // Check if user wants history information in backup
-        if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
-            val historyByMangaId = getHistory.await(manga.id)
-            if (historyByMangaId.isNotEmpty()) {
-                val history = historyByMangaId.map { history ->
-                    val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
-                    BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
-                }
-                if (history.isNotEmpty()) {
-                    mangaObject.history = history
-                }
-            }
-        }
-
-        return mangaObject
-    }
-
-    private fun backupAppPreferences(flags: Int): List<BackupPreference> {
-        if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
-
-        return preferenceStore.getAll().toBackupPreferences()
-    }
-
-    private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
-        if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
-
-        return sourceManager.getOnlineSources()
-            .filterIsInstance<ConfigurableSource>()
-            .map {
-                BackupSourcePreferences(
-                    it.preferenceKey(),
-                    it.sourcePreferences().all.toBackupPreferences()
-                )
-            }
-    }
-
-    @Suppress("UNCHECKED_CAST")
-    private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
-        return this.filterKeys { !Preference.isPrivate(it) }
-            .mapNotNull { (key, value) ->
-                when (value) {
-                    is Int -> BackupPreference(key, IntPreferenceValue(value))
-                    is Long -> BackupPreference(key, LongPreferenceValue(value))
-                    is Float -> BackupPreference(key, FloatPreferenceValue(value))
-                    is String -> BackupPreference(key, StringPreferenceValue(value))
-                    is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
-                    is Set<*> -> (value as? Set<String>)?.let {
-                        BackupPreference(key, StringSetPreferenceValue(it))
-                    }
-                    else -> null
-                }
-            }
-    }
-
-    internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
-        var updatedManga = manga.copy(id = dbManga._id)
-        updatedManga = updatedManga.copyFrom(dbManga)
-        updateManga(updatedManga)
-        return updatedManga
-    }
-
-    /**
-     * Fetches manga information
-     *
-     * @param manga manga that needs updating
-     * @return Updated manga info.
-     */
-    internal suspend fun restoreNewManga(manga: Manga): Manga {
-        return manga.copy(
-            initialized = manga.description != null,
-            id = insertManga(manga),
-        )
-    }
-
-    /**
-     * Restore the categories from Json
-     *
-     * @param backupCategories list containing categories
-     */
-    internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
-        // Get categories from file and from db
-        val dbCategories = getCategories.await()
-
-        val categories = backupCategories.map {
-            var category = it.getCategory()
-            var found = false
-            for (dbCategory in dbCategories) {
-                // If the category is already in the db, assign the id to the file's category
-                // and do nothing
-                if (category.name == dbCategory.name) {
-                    category = category.copy(id = dbCategory.id)
-                    found = true
-                    break
-                }
-            }
-            if (!found) {
-                // Let the db assign the id
-                val id = handler.awaitOneExecutable {
-                    categoriesQueries.insert(category.name, category.order, category.flags)
-                    categoriesQueries.selectLastInsertedRowId()
-                }
-                category = category.copy(id = id)
-            }
-
-            category
-        }
-
-        libraryPreferences.categorizedDisplaySettings().set(
-            (dbCategories + categories)
-                .distinctBy { it.flags }
-                .size > 1,
-        )
-    }
-
-    /**
-     * Restores the categories a manga is in.
-     *
-     * @param manga the manga whose categories have to be restored.
-     * @param categories the categories to restore.
-     */
-    internal suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
-        val dbCategories = getCategories.await()
-        val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
-
-        categories.forEach { backupCategoryOrder ->
-            backupCategories.firstOrNull {
-                it.order == backupCategoryOrder.toLong()
-            }?.let { backupCategory ->
-                dbCategories.firstOrNull { dbCategory ->
-                    dbCategory.name == backupCategory.name
-                }?.let { dbCategory ->
-                    mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
-                }
-            }
-        }
-
-        // Update database
-        if (mangaCategoriesToUpdate.isNotEmpty()) {
-            handler.await(true) {
-                mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
-                mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
-                    mangas_categoriesQueries.insert(mangaId, categoryId)
-                }
-            }
-        }
-    }
-
-    /**
-     * Restore history from Json
-     *
-     * @param history list containing history to be restored
-     */
-    internal suspend fun restoreHistory(history: List<BackupHistory>) {
-        // List containing history to be updated
-        val toUpdate = mutableListOf<HistoryUpdate>()
-        for ((url, lastRead, readDuration) in history) {
-            var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
-            // Check if history already in database and update
-            if (dbHistory != null) {
-                dbHistory = dbHistory.copy(
-                    last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
-                    time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
-                )
-                toUpdate.add(
-                    HistoryUpdate(
-                        chapterId = dbHistory.chapter_id,
-                        readAt = dbHistory.last_read!!,
-                        sessionReadDuration = dbHistory.time_read,
-                    ),
-                )
-            } else {
-                // If not in database create
-                handler
-                    .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
-                    ?.let {
-                        toUpdate.add(
-                            HistoryUpdate(
-                                chapterId = it._id,
-                                readAt = Date(lastRead),
-                                sessionReadDuration = readDuration,
-                            ),
-                        )
-                    }
-            }
-        }
-        handler.await(true) {
-            toUpdate.forEach { payload ->
-                historyQueries.upsert(
-                    payload.chapterId,
-                    payload.readAt,
-                    payload.sessionReadDuration,
-                )
-            }
-        }
-    }
-
-    /**
-     * Restores the sync of a manga.
-     *
-     * @param manga the manga whose sync have to be restored.
-     * @param tracks the track list to restore.
-     */
-    internal suspend fun restoreTracking(manga: Manga, tracks: List<tachiyomi.domain.track.model.Track>) {
-        // Get tracks from database
-        val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
-        val toUpdate = mutableListOf<Manga_sync>()
-        val toInsert = mutableListOf<tachiyomi.domain.track.model.Track>()
-
-        tracks
-            // Fix foreign keys with the current manga id
-            .map { it.copy(mangaId = manga.id) }
-            .forEach { track ->
-                var isInDatabase = false
-                for (dbTrack in dbTracks) {
-                    if (track.syncId == dbTrack.sync_id) {
-                        // The sync is already in the db, only update its fields
-                        var temp = dbTrack
-                        if (track.remoteId != dbTrack.remote_id) {
-                            temp = temp.copy(remote_id = track.remoteId)
-                        }
-                        if (track.libraryId != dbTrack.library_id) {
-                            temp = temp.copy(library_id = track.libraryId)
-                        }
-                        temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
-                        isInDatabase = true
-                        toUpdate.add(temp)
-                        break
-                    }
-                }
-                if (!isInDatabase) {
-                    // Insert new sync. Let the db assign the id
-                    toInsert.add(track.copy(id = 0))
-                }
-            }
-
-        // Update database
-        if (toUpdate.isNotEmpty()) {
-            handler.await(true) {
-                toUpdate.forEach { track ->
-                    manga_syncQueries.update(
-                        track.manga_id,
-                        track.sync_id,
-                        track.remote_id,
-                        track.library_id,
-                        track.title,
-                        track.last_chapter_read,
-                        track.total_chapters,
-                        track.status,
-                        track.score,
-                        track.remote_url,
-                        track.start_date,
-                        track.finish_date,
-                        track._id,
-                    )
-                }
-            }
-        }
-        if (toInsert.isNotEmpty()) {
-            handler.await(true) {
-                toInsert.forEach { track ->
-                    manga_syncQueries.insert(
-                        track.mangaId,
-                        track.syncId,
-                        track.remoteId,
-                        track.libraryId,
-                        track.title,
-                        track.lastChapterRead,
-                        track.totalChapters,
-                        track.status,
-                        track.score,
-                        track.remoteUrl,
-                        track.startDate,
-                        track.finishDate,
-                    )
-                }
-            }
-        }
-    }
-
-    internal suspend fun restoreChapters(manga: Manga, chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
-        val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) }
-
-        val processed = chapters.map { chapter ->
-            var updatedChapter = chapter
-            val dbChapter = dbChapters.find { it.url == updatedChapter.url }
-            if (dbChapter != null) {
-                updatedChapter = updatedChapter.copy(id = dbChapter._id)
-                updatedChapter = updatedChapter.copyFrom(dbChapter)
-                if (dbChapter.read && !updatedChapter.read) {
-                    updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read)
-                } else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
-                    updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
-                }
-                if (!updatedChapter.bookmark && dbChapter.bookmark) {
-                    updatedChapter = updatedChapter.copy(bookmark = true)
-                }
-            }
-
-            updatedChapter.copy(mangaId = manga.id)
-        }
-
-        val newChapters = processed.groupBy { it.id > 0 }
-        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.awaitOneExecutable(true) {
-            mangasQueries.insert(
-                source = manga.source,
-                url = manga.url,
-                artist = manga.artist,
-                author = manga.author,
-                description = manga.description,
-                genre = manga.genre,
-                title = manga.title,
-                status = manga.status,
-                thumbnailUrl = manga.thumbnailUrl,
-                favorite = manga.favorite,
-                lastUpdate = manga.lastUpdate,
-                nextUpdate = 0L,
-                calculateInterval = 0L,
-                initialized = manga.initialized,
-                viewerFlags = manga.viewerFlags,
-                chapterFlags = manga.chapterFlags,
-                coverLastModified = manga.coverLastModified,
-                dateAdded = manga.dateAdded,
-                updateStrategy = manga.updateStrategy,
-            )
-            mangasQueries.selectLastInsertedRowId()
-        }
-    }
-
-    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?.joinToString(separator = ", "),
-                title = manga.title,
-                status = manga.status,
-                thumbnailUrl = manga.thumbnailUrl,
-                favorite = manga.favorite,
-                lastUpdate = manga.lastUpdate,
-                nextUpdate = null,
-                calculateInterval = null,
-                initialized = manga.initialized,
-                viewer = manga.viewerFlags,
-                chapterFlags = manga.chapterFlags,
-                coverLastModified = manga.coverLastModified,
-                dateAdded = manga.dateAdded,
-                mangaId = manga.id,
-                updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
-            )
-        }
-        return manga.id
-    }
-
-    /**
-     * Inserts list of chapters
-     */
-    private suspend fun insertChapters(chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
-        handler.await(true) {
-            chapters.forEach { chapter ->
-                chaptersQueries.insert(
-                    chapter.mangaId,
-                    chapter.url,
-                    chapter.name,
-                    chapter.scanlator,
-                    chapter.read,
-                    chapter.bookmark,
-                    chapter.lastPageRead,
-                    chapter.chapterNumber,
-                    chapter.sourceOrder,
-                    chapter.dateFetch,
-                    chapter.dateUpload,
-                )
-            }
-        }
-    }
-
-    /**
-     * Updates a list of chapters with known database ids
-     */
-    private suspend fun updateKnownChapters(chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
-        handler.await(true) {
-            chapters.forEach { chapter ->
-                chaptersQueries.update(
-                    mangaId = null,
-                    url = null,
-                    name = null,
-                    scanlator = null,
-                    read = chapter.read,
-                    bookmark = chapter.bookmark,
-                    lastPageRead = chapter.lastPageRead,
-                    chapterNumber = null,
-                    sourceOrder = null,
-                    dateFetch = null,
-                    dateUpload = null,
-                    chapterId = chapter.id,
-                )
-            }
-        }
-    }
-}

+ 390 - 12
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt

@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup
 
 import android.content.Context
 import android.net.Uri
+import eu.kanade.domain.chapter.model.copyFrom
 import eu.kanade.domain.manga.interactor.UpdateManga
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.backup.models.BackupCategory
@@ -16,6 +17,7 @@ import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
 import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
 import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
 import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
+import eu.kanade.tachiyomi.source.model.copyFrom
 import eu.kanade.tachiyomi.source.sourcePreferences
 import eu.kanade.tachiyomi.util.BackupUtil
 import eu.kanade.tachiyomi.util.system.createFileInCacheDir
@@ -23,7 +25,14 @@ import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.isActive
 import tachiyomi.core.preference.AndroidPreferenceStore
 import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.data.DatabaseHandler
+import tachiyomi.data.Manga_sync
+import tachiyomi.data.Mangas
+import tachiyomi.data.UpdateStrategyColumnAdapter
+import tachiyomi.domain.category.interactor.GetCategories
 import tachiyomi.domain.chapter.model.Chapter
+import tachiyomi.domain.history.model.HistoryUpdate
+import tachiyomi.domain.library.service.LibraryPreferences
 import tachiyomi.domain.manga.interactor.FetchInterval
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.track.model.Track
@@ -34,20 +43,24 @@ import java.text.SimpleDateFormat
 import java.time.ZonedDateTime
 import java.util.Date
 import java.util.Locale
+import kotlin.math.max
 
 class BackupRestorer(
     private val context: Context,
     private val notifier: BackupNotifier,
 ) {
+
+    private val handler: DatabaseHandler = Injekt.get()
     private val updateManga: UpdateManga = Injekt.get()
+    private val getCategories: GetCategories = Injekt.get()
     private val fetchInterval: FetchInterval = Injekt.get()
+
     private val preferenceStore: PreferenceStore = Injekt.get()
+    private val libraryPreferences: LibraryPreferences = Injekt.get()
 
     private var now = ZonedDateTime.now()
     private var currentFetchWindow = fetchInterval.getWindow(now)
 
-    private var backupManager = BackupManager(context)
-
     private var restoreAmount = 0
     private var restoreProgress = 0
 
@@ -102,7 +115,7 @@ class BackupRestorer(
     private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
         val backup = BackupUtil.decodeBackup(context, uri)
 
-        restoreAmount = backup.backupManga.size + 1 // +1 for categories
+        restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
 
         // Restore categories
         if (backup.backupCategories.isNotEmpty()) {
@@ -134,7 +147,38 @@ class BackupRestorer(
     }
 
     private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
-        backupManager.restoreCategories(backupCategories)
+        // Get categories from file and from db
+        val dbCategories = getCategories.await()
+
+        val categories = backupCategories.map {
+            var category = it.getCategory()
+            var found = false
+            for (dbCategory in dbCategories) {
+                // If the category is already in the db, assign the id to the file's category
+                // and do nothing
+                if (category.name == dbCategory.name) {
+                    category = category.copy(id = dbCategory.id)
+                    found = true
+                    break
+                }
+            }
+            if (!found) {
+                // Let the db assign the id
+                val id = handler.awaitOneExecutable {
+                    categoriesQueries.insert(category.name, category.order, category.flags)
+                    categoriesQueries.selectLastInsertedRowId()
+                }
+                category = category.copy(id = id)
+            }
+
+            category
+        }
+
+        libraryPreferences.categorizedDisplaySettings().set(
+            (dbCategories + categories)
+                .distinctBy { it.flags }
+                .size > 1,
+        )
 
         restoreProgress += 1
         showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup))
@@ -149,14 +193,14 @@ class BackupRestorer(
         val tracks = backupManga.getTrackingImpl()
 
         try {
-            val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
+            val dbManga = getMangaFromDatabase(manga.url, manga.source)
             val restoredManga = if (dbManga == null) {
                 // Manga not in database
                 restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
             } else {
                 // Manga in database
                 // Copy information from manga already in database
-                val updatedManga = backupManager.restoreExistingManga(manga, dbManga)
+                val updatedManga = restoreExistingManga(manga, dbManga)
                 // Fetch rest of manga information
                 restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
             }
@@ -174,6 +218,50 @@ class BackupRestorer(
         }
     }
 
+    /**
+     * Returns manga
+     *
+     * @return [Manga], null if not found
+     */
+    private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
+        return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
+    }
+
+    private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
+        var updatedManga = manga.copy(id = dbManga._id)
+        updatedManga = updatedManga.copyFrom(dbManga)
+        updateManga(updatedManga)
+        return updatedManga
+    }
+
+    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?.joinToString(separator = ", "),
+                title = manga.title,
+                status = manga.status,
+                thumbnailUrl = manga.thumbnailUrl,
+                favorite = manga.favorite,
+                lastUpdate = manga.lastUpdate,
+                nextUpdate = null,
+                calculateInterval = null,
+                initialized = manga.initialized,
+                viewer = manga.viewerFlags,
+                chapterFlags = manga.chapterFlags,
+                coverLastModified = manga.coverLastModified,
+                dateAdded = manga.dateAdded,
+                mangaId = manga.id,
+                updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
+            )
+        }
+        return manga.id
+    }
+
     /**
      * Fetches manga information
      *
@@ -189,12 +277,131 @@ class BackupRestorer(
         tracks: List<Track>,
         backupCategories: List<BackupCategory>,
     ): Manga {
-        val fetchedManga = backupManager.restoreNewManga(manga)
-        backupManager.restoreChapters(fetchedManga, chapters)
+        val fetchedManga = restoreNewManga(manga)
+        restoreChapters(fetchedManga, chapters)
         restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
         return fetchedManga
     }
 
+    private suspend fun restoreChapters(manga: Manga, chapters: List<Chapter>) {
+        val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) }
+
+        val processed = chapters.map { chapter ->
+            var updatedChapter = chapter
+            val dbChapter = dbChapters.find { it.url == updatedChapter.url }
+            if (dbChapter != null) {
+                updatedChapter = updatedChapter.copy(id = dbChapter._id)
+                updatedChapter = updatedChapter.copyFrom(dbChapter)
+                if (dbChapter.read && !updatedChapter.read) {
+                    updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read)
+                } else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
+                    updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
+                }
+                if (!updatedChapter.bookmark && dbChapter.bookmark) {
+                    updatedChapter = updatedChapter.copy(bookmark = true)
+                }
+            }
+
+            updatedChapter.copy(mangaId = manga.id)
+        }
+
+        val newChapters = processed.groupBy { it.id > 0 }
+        newChapters[true]?.let { updateKnownChapters(it) }
+        newChapters[false]?.let { insertChapters(it) }
+    }
+
+    /**
+     * Inserts list of chapters
+     */
+    private suspend fun insertChapters(chapters: List<Chapter>) {
+        handler.await(true) {
+            chapters.forEach { chapter ->
+                chaptersQueries.insert(
+                    chapter.mangaId,
+                    chapter.url,
+                    chapter.name,
+                    chapter.scanlator,
+                    chapter.read,
+                    chapter.bookmark,
+                    chapter.lastPageRead,
+                    chapter.chapterNumber,
+                    chapter.sourceOrder,
+                    chapter.dateFetch,
+                    chapter.dateUpload,
+                )
+            }
+        }
+    }
+
+    /**
+     * 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,
+                    bookmark = chapter.bookmark,
+                    lastPageRead = chapter.lastPageRead,
+                    chapterNumber = null,
+                    sourceOrder = null,
+                    dateFetch = null,
+                    dateUpload = null,
+                    chapterId = chapter.id,
+                )
+            }
+        }
+    }
+
+    /**
+     * Fetches manga information
+     *
+     * @param manga manga that needs updating
+     * @return Updated manga info.
+     */
+    private suspend fun restoreNewManga(manga: Manga): Manga {
+        return manga.copy(
+            initialized = manga.description != null,
+            id = insertManga(manga),
+        )
+    }
+
+    /**
+     * Inserts manga and returns id
+     *
+     * @return id of [Manga], null if not found
+     */
+    private suspend fun insertManga(manga: Manga): Long {
+        return handler.awaitOneExecutable(true) {
+            mangasQueries.insert(
+                source = manga.source,
+                url = manga.url,
+                artist = manga.artist,
+                author = manga.author,
+                description = manga.description,
+                genre = manga.genre,
+                title = manga.title,
+                status = manga.status,
+                thumbnailUrl = manga.thumbnailUrl,
+                favorite = manga.favorite,
+                lastUpdate = manga.lastUpdate,
+                nextUpdate = 0L,
+                calculateInterval = 0L,
+                initialized = manga.initialized,
+                viewerFlags = manga.viewerFlags,
+                chapterFlags = manga.chapterFlags,
+                coverLastModified = manga.coverLastModified,
+                dateAdded = manga.dateAdded,
+                updateStrategy = manga.updateStrategy,
+            )
+            mangasQueries.selectLastInsertedRowId()
+        }
+    }
+
     private suspend fun restoreNewManga(
         backupManga: Manga,
         chapters: List<Chapter>,
@@ -203,19 +410,187 @@ class BackupRestorer(
         tracks: List<Track>,
         backupCategories: List<BackupCategory>,
     ): Manga {
-        backupManager.restoreChapters(backupManga, chapters)
+        restoreChapters(backupManga, chapters)
         restoreExtras(backupManga, categories, history, tracks, backupCategories)
         return backupManga
     }
 
     private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
-        backupManager.restoreCategories(manga, categories, backupCategories)
-        backupManager.restoreHistory(history)
-        backupManager.restoreTracking(manga, tracks)
+        restoreCategories(manga, categories, backupCategories)
+        restoreHistory(history)
+        restoreTracking(manga, tracks)
+    }
+
+    /**
+     * Restores the categories a manga is in.
+     *
+     * @param manga the manga whose categories have to be restored.
+     * @param categories the categories to restore.
+     */
+    private suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
+        val dbCategories = getCategories.await()
+        val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
+
+        categories.forEach { backupCategoryOrder ->
+            backupCategories.firstOrNull {
+                it.order == backupCategoryOrder.toLong()
+            }?.let { backupCategory ->
+                dbCategories.firstOrNull { dbCategory ->
+                    dbCategory.name == backupCategory.name
+                }?.let { dbCategory ->
+                    mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
+                }
+            }
+        }
+
+        // Update database
+        if (mangaCategoriesToUpdate.isNotEmpty()) {
+            handler.await(true) {
+                mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
+                mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
+                    mangas_categoriesQueries.insert(mangaId, categoryId)
+                }
+            }
+        }
+    }
+
+    /**
+     * Restore history from Json
+     *
+     * @param history list containing history to be restored
+     */
+    private suspend fun restoreHistory(history: List<BackupHistory>) {
+        // List containing history to be updated
+        val toUpdate = mutableListOf<HistoryUpdate>()
+        for ((url, lastRead, readDuration) in history) {
+            var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
+            // Check if history already in database and update
+            if (dbHistory != null) {
+                dbHistory = dbHistory.copy(
+                    last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
+                    time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
+                )
+                toUpdate.add(
+                    HistoryUpdate(
+                        chapterId = dbHistory.chapter_id,
+                        readAt = dbHistory.last_read!!,
+                        sessionReadDuration = dbHistory.time_read,
+                    ),
+                )
+            } else {
+                // If not in database create
+                handler
+                    .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
+                    ?.let {
+                        toUpdate.add(
+                            HistoryUpdate(
+                                chapterId = it._id,
+                                readAt = Date(lastRead),
+                                sessionReadDuration = readDuration,
+                            ),
+                        )
+                    }
+            }
+        }
+        handler.await(true) {
+            toUpdate.forEach { payload ->
+                historyQueries.upsert(
+                    payload.chapterId,
+                    payload.readAt,
+                    payload.sessionReadDuration,
+                )
+            }
+        }
+    }
+
+    /**
+     * Restores the sync of a manga.
+     *
+     * @param manga the manga whose sync have to be restored.
+     * @param tracks the track list to restore.
+     */
+    private suspend fun restoreTracking(manga: Manga, tracks: List<Track>) {
+        // Get tracks from database
+        val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
+        val toUpdate = mutableListOf<Manga_sync>()
+        val toInsert = mutableListOf<Track>()
+
+        tracks
+            // Fix foreign keys with the current manga id
+            .map { it.copy(mangaId = manga.id) }
+            .forEach { track ->
+                var isInDatabase = false
+                for (dbTrack in dbTracks) {
+                    if (track.syncId == dbTrack.sync_id) {
+                        // The sync is already in the db, only update its fields
+                        var temp = dbTrack
+                        if (track.remoteId != dbTrack.remote_id) {
+                            temp = temp.copy(remote_id = track.remoteId)
+                        }
+                        if (track.libraryId != dbTrack.library_id) {
+                            temp = temp.copy(library_id = track.libraryId)
+                        }
+                        temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
+                        isInDatabase = true
+                        toUpdate.add(temp)
+                        break
+                    }
+                }
+                if (!isInDatabase) {
+                    // Insert new sync. Let the db assign the id
+                    toInsert.add(track.copy(id = 0))
+                }
+            }
+
+        // Update database
+        if (toUpdate.isNotEmpty()) {
+            handler.await(true) {
+                toUpdate.forEach { track ->
+                    manga_syncQueries.update(
+                        track.manga_id,
+                        track.sync_id,
+                        track.remote_id,
+                        track.library_id,
+                        track.title,
+                        track.last_chapter_read,
+                        track.total_chapters,
+                        track.status,
+                        track.score,
+                        track.remote_url,
+                        track.start_date,
+                        track.finish_date,
+                        track._id,
+                    )
+                }
+            }
+        }
+        if (toInsert.isNotEmpty()) {
+            handler.await(true) {
+                toInsert.forEach { track ->
+                    manga_syncQueries.insert(
+                        track.mangaId,
+                        track.syncId,
+                        track.remoteId,
+                        track.libraryId,
+                        track.title,
+                        track.lastChapterRead,
+                        track.totalChapters,
+                        track.status,
+                        track.score,
+                        track.remoteUrl,
+                        track.startDate,
+                        track.finishDate,
+                    )
+                }
+            }
+        }
     }
 
     private fun restoreAppPreferences(preferences: List<BackupPreference>) {
         restorePreferences(preferences, preferenceStore)
+
+        restoreProgress += 1
+        showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.app_settings), context.getString(R.string.restoring_backup))
     }
 
     private fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
@@ -223,6 +598,9 @@ class BackupRestorer(
             val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
             restorePreferences(it.prefs, sourcePrefs)
         }
+
+        restoreProgress += 1
+        showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.source_settings), context.getString(R.string.restoring_backup))
     }
 
     private fun restorePreferences(

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

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
 
 import android.content.Context
 import android.net.Uri
-import eu.kanade.tachiyomi.data.backup.BackupManager
+import eu.kanade.tachiyomi.data.backup.BackupCreator
 import eu.kanade.tachiyomi.data.backup.models.Backup
 import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
 import okio.buffer
@@ -14,7 +14,7 @@ object BackupUtil {
      * Decode a potentially-gzipped backup.
      */
     fun decodeBackup(context: Context, uri: Uri): Backup {
-        val backupManager = BackupManager(context)
+        val backupCreator = BackupCreator(context)
 
         val backupStringSource = context.contentResolver.openInputStream(uri)!!.source().buffer()
 
@@ -27,6 +27,6 @@ object BackupUtil {
             backupStringSource
         }.use { it.readByteArray() }
 
-        return backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
+        return backupCreator.parser.decodeFromByteArray(BackupSerializer, backupString)
     }
 }