From cd16522805eedc73fa3ab0f8db5ee403162a020c Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 16 Dec 2023 11:43:18 -0500 Subject: [PATCH] Split restoring logic into smaller classes --- .../settings/screen/SettingsDataScreen.kt | 4 +- .../screen/data/CreateBackupScreen.kt | 4 +- .../java/eu/kanade/tachiyomi/Migrations.kt | 2 +- .../backup/{ => create}/BackupCreateFlags.kt | 2 +- .../backup/{ => create}/BackupCreateJob.kt | 4 +- .../data/backup/{ => create}/BackupCreator.kt | 15 +- .../data/backup/models/BackupSource.kt | 4 +- .../backup/{ => restore}/BackupRestoreJob.kt | 8 +- .../data/backup/restore/BackupRestorer.kt | 156 +++++++++++ .../data/backup/restore/CategoriesRestorer.kt | 36 +++ .../MangaRestorer.kt} | 260 ++---------------- .../data/backup/restore/PreferenceRestorer.kt | 79 ++++++ .../data/notification/NotificationReceiver.kt | 2 +- .../eu/kanade/tachiyomi/util/BackupUtil.kt | 2 +- 14 files changed, 318 insertions(+), 260 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => create}/BackupCreateFlags.kt (90%) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => create}/BackupCreateJob.kt (96%) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => create}/BackupCreator.kt (94%) rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{ => restore}/BackupRestoreJob.kt (92%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt rename app/src/main/java/eu/kanade/tachiyomi/data/backup/{BackupRestorer.kt => restore/MangaRestorer.kt} (61%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index eef754b693..1a5c93132c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -37,9 +37,9 @@ import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.util.relativeTimeSpanString -import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupFileValidator -import eu.kanade.tachiyomi.data.backup.BackupRestoreJob +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.DeviceUtil diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 96c6ead6a0..261a1b20ac 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -29,8 +29,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.util.Screen -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags -import eu.kanade.tachiyomi.data.backup.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 022dcefcda..775bfe78dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -7,7 +7,7 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences -import eu.kanade.tachiyomi.data.backup.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.network.NetworkPreferences diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt similarity index 90% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt index 7ae6edfde9..25c843a0d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.create internal object BackupCreateFlags { const val BACKUP_CATEGORY = 0x1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt similarity index 96% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt index f314d9c019..e4fcf6cd05 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.create import android.content.Context import android.net.Uri @@ -14,6 +14,8 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkerParameters import androidx.work.workDataOf import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.data.backup.BackupNotifier +import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.isRunning diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index b578742a9a..66dd19bbdd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -1,14 +1,15 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.create import android.content.Context import android.net.Uri import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK +import eu.kanade.tachiyomi.data.backup.BackupFileValidator +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CHAPTER +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_TRACK import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupChapter diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt index 7bf2d0bc35..34e4cac313 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt @@ -8,7 +8,9 @@ import kotlinx.serialization.protobuf.ProtoNumber data class BrokenBackupSource( @ProtoNumber(0) var name: String = "", @ProtoNumber(1) var sourceId: Long, -) +) { + fun toBackupSource() = BackupSource(name, sourceId) +} @Serializable data class BackupSource( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt similarity index 92% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt index e4595a4b5f..507993d9ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.restore import android.content.Context import android.net.Uri @@ -9,6 +9,7 @@ import androidx.work.ForegroundInfo import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkerParameters import androidx.work.workDataOf +import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.isRunning @@ -28,13 +29,12 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet override suspend fun doWork(): Result { val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: return Result.failure() - val sync = inputData.getBoolean(SYNC_KEY, false) + val isSync = inputData.getBoolean(SYNC_KEY, false) setForegroundSafely() return try { - val restorer = BackupRestorer(context, notifier) - restorer.syncFromBackup(uri, sync) + BackupRestorer(context, notifier, isSync).restore(uri) Result.success() } catch (e: Exception) { if (e is CancellationException) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt new file mode 100644 index 0000000000..a1e2628442 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -0,0 +1,156 @@ +package eu.kanade.tachiyomi.data.backup.restore + +import android.content.Context +import android.net.Uri +import eu.kanade.tachiyomi.data.backup.BackupNotifier +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupPreference +import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.util.BackupUtil +import eu.kanade.tachiyomi.util.system.createFileInCacheDir +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import tachiyomi.core.i18n.stringResource +import tachiyomi.i18n.MR +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class BackupRestorer( + private val context: Context, + private val notifier: BackupNotifier, + private val isSync: Boolean, + + private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), + private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), + private val mangaRestorer: MangaRestorer = MangaRestorer(), +) { + + private var restoreAmount = 0 + private var restoreProgress = 0 + private val errors = mutableListOf>() + + /** + * Mapping of source ID to source name from backup data + */ + private var sourceMapping: Map = emptyMap() + + suspend fun restore(uri: Uri) { + val startTime = System.currentTimeMillis() + + restoreFromFile(uri) + + val time = System.currentTimeMillis() - startTime + + val logFile = writeErrorLog() + + notifier.showRestoreComplete( + time, + errors.size, + logFile.parent, + logFile.name, + isSync, + ) + } + + private suspend fun restoreFromFile(uri: Uri) { + val backup = BackupUtil.decodeBackup(context, uri) + + restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs + + // Store source mapping for error messages + val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() } + sourceMapping = backupMaps.associate { it.sourceId to it.name } + + coroutineScope { + restoreCategories(backup.backupCategories) + restoreAppPreferences(backup.backupPreferences) + restoreSourcePreferences(backup.backupSourcePreferences) + restoreManga(backup.backupManga, backup.backupCategories) + + // TODO: optionally trigger online library + tracker update + } + } + + private fun CoroutineScope.restoreCategories(backupCategories: List) = launch { + ensureActive() + categoriesRestorer.restoreCategories(backupCategories) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.categories), + restoreProgress, + restoreAmount, + isSync, + ) + } + + private fun CoroutineScope.restoreManga( + backupMangas: List, + backupCategories: List, + ) = launch { + mangaRestorer.sortByNew(backupMangas) + .forEach { + ensureActive() + + try { + mangaRestorer.restoreManga(it, backupCategories) + } catch (e: Exception) { + val sourceName = sourceMapping[it.source] ?: it.source.toString() + errors.add(Date() to "${it.title} [$sourceName]: ${e.message}") + } + + restoreProgress += 1 + notifier.showRestoreProgress(it.title, restoreProgress, restoreAmount, isSync) + } + } + + private fun CoroutineScope.restoreAppPreferences(preferences: List) = launch { + ensureActive() + preferenceRestorer.restoreAppPreferences(preferences) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.app_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } + + private fun CoroutineScope.restoreSourcePreferences(preferences: List) = launch { + ensureActive() + preferenceRestorer.restoreSourcePreferences(preferences) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.source_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } + + private 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("") + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt new file mode 100644 index 0000000000..5557bb59f3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.data.backup.restore + +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import tachiyomi.data.DatabaseHandler +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.library.service.LibraryPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class CategoriesRestorer( + private val handler: DatabaseHandler = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), +) { + + suspend fun restoreCategories(backupCategories: List) { + if (backupCategories.isNotEmpty()) { + val dbCategories = getCategories.await() + val dbCategoriesByName = dbCategories.associateBy { it.name } + + val categories = backupCategories.map { + dbCategoriesByName[it.name] + ?: handler.awaitOneExecutable { + categoriesQueries.insert(it.name, it.order, it.flags) + categoriesQueries.selectLastInsertedRowId() + }.let { id -> it.toCategory(id) } + } + + libraryPreferences.categorizedDisplaySettings().set( + (dbCategories + categories) + .distinctBy { it.flags } + .size > 1, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt similarity index 61% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt index 55488ca86b..a07f846f23 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt @@ -1,57 +1,29 @@ -package eu.kanade.tachiyomi.data.backup +package eu.kanade.tachiyomi.data.backup.restore -import android.content.Context -import android.net.Uri import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupChapter 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.BackupSource -import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import eu.kanade.tachiyomi.data.backup.models.BackupTracking -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.library.LibraryUpdateJob -import eu.kanade.tachiyomi.source.sourcePreferences -import eu.kanade.tachiyomi.util.BackupUtil -import eu.kanade.tachiyomi.util.system.createFileInCacheDir -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.ensureActive -import tachiyomi.core.i18n.stringResource -import tachiyomi.core.preference.AndroidPreferenceStore -import tachiyomi.core.preference.PreferenceStore import tachiyomi.data.DatabaseHandler import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.model.Track -import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File -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, - +class MangaRestorer( private val handler: DatabaseHandler = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), @@ -59,167 +31,48 @@ class BackupRestorer( private val updateManga: UpdateManga = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(), - private val fetchInterval: FetchInterval = Injekt.get(), - - private val preferenceStore: PreferenceStore = Injekt.get(), - private val libraryPreferences: LibraryPreferences = Injekt.get(), + fetchInterval: FetchInterval = Injekt.get(), ) { - private var restoreAmount = 0 - private var restoreProgress = 0 - private var now = ZonedDateTime.now() private var currentFetchWindow = fetchInterval.getWindow(now) - /** - * Mapping of source ID to source name from backup data - */ - private var sourceMapping: Map = emptyMap() - - private val errors = mutableListOf>() - - suspend fun syncFromBackup(uri: Uri, sync: Boolean) { - val startTime = System.currentTimeMillis() - - prepareState() - restoreFromFile(uri, sync) - - val endTime = System.currentTimeMillis() - val time = endTime - startTime - - val logFile = writeErrorLog() - - notifier.showRestoreComplete( - time, - errors.size, - logFile.parent, - logFile.name, - sync, - ) - } - - private 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("") - } - - private fun prepareState() { + init { now = ZonedDateTime.now() currentFetchWindow = fetchInterval.getWindow(now) } - private suspend fun restoreFromFile(uri: Uri, sync: Boolean) { - val backup = BackupUtil.decodeBackup(context, uri) - - restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs - - // Store source mapping for error messages - val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources - sourceMapping = backupMaps.associate { it.sourceId to it.name } - - coroutineScope { - ensureActive() - restoreCategories(backup.backupCategories) - - ensureActive() - restoreAppPreferences(backup.backupPreferences) - - ensureActive() - restoreSourcePreferences(backup.backupSourcePreferences) - - backup.backupManga.sortByNew() - .forEach { - ensureActive() - restoreManga(it, backup.backupCategories, sync) - } - - // TODO: optionally trigger online library + tracker update - } - } - - private suspend fun List.sortByNew(): List { + suspend fun sortByNew(backupMangas: List): List { val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() } .groupBy({ it.source }, { it.url }) - return this + return backupMangas .sortedWith( compareBy { it.url in urlsBySource[it.source].orEmpty() } .then(compareByDescending { it.lastModifiedAt }), ) } - private suspend fun restoreCategories(backupCategories: List) { - if (backupCategories.isNotEmpty()) { - val dbCategories = getCategories.await() - val dbCategoriesByName = dbCategories.associateBy { it.name } - - val categories = backupCategories.map { - dbCategoriesByName[it.name] - ?: handler.awaitOneExecutable { - categoriesQueries.insert(it.name, it.order, it.flags) - categoriesQueries.selectLastInsertedRowId() - }.let { id -> it.toCategory(id) } - } - - libraryPreferences.categorizedDisplaySettings().set( - (dbCategories + categories) - .distinctBy { it.flags } - .size > 1, - ) - } - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.categories), - restoreProgress, - restoreAmount, - false, - ) - } - - private suspend fun restoreManga( + suspend fun restoreManga( backupManga: BackupManga, backupCategories: List, - sync: Boolean, ) { - try { - val dbManga = findExistingManga(backupManga) - val manga = backupManga.getMangaImpl() - val restoredManga = if (dbManga == null) { - restoreNewManga(manga) - } else { - restoreExistingManga(manga, dbManga) - } - - restoreMangaDetails( - manga = restoredManga, - chapters = backupManga.chapters, - categories = backupManga.categories, - backupCategories = backupCategories, - history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, - tracks = backupManga.tracking, - ) - } catch (e: Exception) { - val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString() - errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}") + val dbManga = findExistingManga(backupManga) + val manga = backupManga.getMangaImpl() + val restoredManga = if (dbManga == null) { + restoreNewManga(manga) + } else { + restoreExistingManga(manga, dbManga) } - restoreProgress += 1 - notifier.showRestoreProgress(backupManga.title, restoreProgress, restoreAmount, sync) + restoreMangaDetails( + manga = restoredManga, + chapters = backupManga.chapters, + categories = backupManga.categories, + backupCategories = backupCategories, + history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() }, + tracks = backupManga.tracking, + ) } private suspend fun findExistingManga(backupManga: BackupManga): Manga? { @@ -546,75 +399,4 @@ class BackupRestorer( } private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L) - - private fun restoreAppPreferences(preferences: List) { - restorePreferences(preferences, preferenceStore) - - LibraryUpdateJob.setupTask(context) - BackupCreateJob.setupTask(context) - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.app_settings), - restoreProgress, - restoreAmount, - false, - ) - } - - private fun restoreSourcePreferences(preferences: List) { - preferences.forEach { - val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) - restorePreferences(it.prefs, sourcePrefs) - } - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(MR.strings.source_settings), - restoreProgress, - restoreAmount, - false, - ) - } - - private fun restorePreferences( - toRestore: List, - preferenceStore: PreferenceStore, - ) { - val prefs = preferenceStore.getAll() - toRestore.forEach { (key, value) -> - when (value) { - is IntPreferenceValue -> { - if (prefs[key] is Int?) { - preferenceStore.getInt(key).set(value.value) - } - } - is LongPreferenceValue -> { - if (prefs[key] is Long?) { - preferenceStore.getLong(key).set(value.value) - } - } - is FloatPreferenceValue -> { - if (prefs[key] is Float?) { - preferenceStore.getFloat(key).set(value.value) - } - } - is StringPreferenceValue -> { - if (prefs[key] is String?) { - preferenceStore.getString(key).set(value.value) - } - } - is BooleanPreferenceValue -> { - if (prefs[key] is Boolean?) { - preferenceStore.getBoolean(key).set(value.value) - } - } - is StringSetPreferenceValue -> { - if (prefs[key] is Set<*>?) { - preferenceStore.getStringSet(key).set(value.value) - } - } - } - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt new file mode 100644 index 0000000000..69622d60b5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.data.backup.restore + +import android.content.Context +import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.models.BackupPreference +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.library.LibraryUpdateJob +import eu.kanade.tachiyomi.source.sourcePreferences +import tachiyomi.core.preference.AndroidPreferenceStore +import tachiyomi.core.preference.PreferenceStore +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class PreferenceRestorer( + private val context: Context, + private val preferenceStore: PreferenceStore = Injekt.get(), +) { + + fun restoreAppPreferences(preferences: List) { + restorePreferences(preferences, preferenceStore) + + LibraryUpdateJob.setupTask(context) + BackupCreateJob.setupTask(context) + } + + fun restoreSourcePreferences(preferences: List) { + preferences.forEach { + val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) + restorePreferences(it.prefs, sourcePrefs) + } + } + + private fun restorePreferences( + toRestore: List, + preferenceStore: PreferenceStore, + ) { + val prefs = preferenceStore.getAll() + toRestore.forEach { (key, value) -> + when (value) { + is IntPreferenceValue -> { + if (prefs[key] is Int?) { + preferenceStore.getInt(key).set(value.value) + } + } + is LongPreferenceValue -> { + if (prefs[key] is Long?) { + preferenceStore.getLong(key).set(value.value) + } + } + is FloatPreferenceValue -> { + if (prefs[key] is Float?) { + preferenceStore.getFloat(key).set(value.value) + } + } + is StringPreferenceValue -> { + if (prefs[key] is String?) { + preferenceStore.getString(key).set(value.value) + } + } + is BooleanPreferenceValue -> { + if (prefs[key] is Boolean?) { + preferenceStore.getBoolean(key).set(value.value) + } + } + is StringSetPreferenceValue -> { + if (prefs[key] is Set<*>?) { + preferenceStore.getStringSet(key).set(value.value) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index f4262a1eac..92d5311d53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -7,7 +7,7 @@ import android.content.Intent import android.net.Uri import android.os.Build import androidx.core.net.toUri -import eu.kanade.tachiyomi.data.backup.BackupRestoreJob +import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt index e67d7cd2fc..05401603fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt +++ b/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.BackupCreator +import eu.kanade.tachiyomi.data.backup.create.BackupCreator import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import okio.buffer