refactor: Rewrite migrator

Should also fixed auto-backup, auto-update, and update checker being stuck when it failed

Co-authored-by: Andreas <andreas.everos@gmail.com>
This commit is contained in:
Ahmad Ansori Palembani 2024-06-15 11:34:29 +07:00
parent c17be3831c
commit 573015a4b9
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
39 changed files with 977 additions and 347 deletions

View file

@ -0,0 +1,22 @@
package dev.yokai.core.migration
interface Migration {
val version: Float
suspend operator fun invoke(migrationContext: MigrationContext): Boolean
val isAlways: Boolean
get() = version == ALWAYS
companion object {
const val ALWAYS = -1f
fun of(version: Float, action: suspend (MigrationContext) -> Boolean): Migration = object : Migration {
override val version: Float = version
override suspend operator fun invoke(migrationContext: MigrationContext): Boolean {
return action(migrationContext)
}
}
}
}

View file

@ -0,0 +1,3 @@
package dev.yokai.core.migration
typealias MigrationCompletedListener = () -> Unit

View file

@ -0,0 +1,10 @@
package dev.yokai.core.migration
import uy.kohesive.injekt.Injekt
class MigrationContext(val dryRun: Boolean) {
inline fun <reified T> get(): T? {
return Injekt.getInstanceOrNull(T::class.java)
}
}

View file

@ -0,0 +1,29 @@
package dev.yokai.core.migration
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
class MigrationJobFactory(
private val migrationContext: MigrationContext,
private val scope: CoroutineScope
) {
fun create(migrations: List<Migration>): Deferred<Boolean> = with(scope) {
return migrations.sortedBy { it.version }
.fold(CompletableDeferred(true)) { acc: Deferred<Boolean>, migration: Migration ->
if (!migrationContext.dryRun) {
Logger.i { "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
async {
val prev = acc.await()
migration(migrationContext) || prev
}
} else {
Logger.i { "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
CompletableDeferred(true)
}
}
}
}

View file

@ -0,0 +1,55 @@
package dev.yokai.core.migration
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch
interface MigrationStrategy {
operator fun invoke(migrations: List<Migration>): Deferred<Boolean>
}
class DefaultMigrationStrategy(
private val migrationJobFactory: MigrationJobFactory,
private val migrationCompletedListener: MigrationCompletedListener,
private val scope: CoroutineScope
) : MigrationStrategy {
override operator fun invoke(migrations: List<Migration>): Deferred<Boolean> = with(scope) {
if (migrations.isEmpty()) {
return@with CompletableDeferred(false)
}
val chain = migrationJobFactory.create(migrations)
launch {
if (chain.await()) migrationCompletedListener()
}.start()
chain
}
}
class InitialMigrationStrategy(private val strategy: DefaultMigrationStrategy) : MigrationStrategy {
override operator fun invoke(migrations: List<Migration>): Deferred<Boolean> {
return strategy(migrations.filter { it.isAlways })
}
}
class NoopMigrationStrategy(val state: Boolean) : MigrationStrategy {
override fun invoke(migrations: List<Migration>): Deferred<Boolean> {
return CompletableDeferred(state)
}
}
class VersionRangeMigrationStrategy(
private val versions: IntRange,
private val strategy: DefaultMigrationStrategy
) : MigrationStrategy {
override operator fun invoke(migrations: List<Migration>): Deferred<Boolean> {
return strategy(migrations.filter { it.isAlways || it.version.toInt() in versions })
}
}

View file

@ -0,0 +1,23 @@
package dev.yokai.core.migration
class MigrationStrategyFactory(
private val factory: MigrationJobFactory,
private val migrationCompletedListener: MigrationCompletedListener,
) {
fun create(old: Int, new: Int): MigrationStrategy {
val versions = (old + 1)..new
val strategy = when {
old == 0 -> InitialMigrationStrategy(
strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope),
)
old >= new -> NoopMigrationStrategy(false)
else -> VersionRangeMigrationStrategy(
versions = versions,
strategy = DefaultMigrationStrategy(factory, migrationCompletedListener, Migrator.scope),
)
}
return strategy
}
}

View file

@ -0,0 +1,40 @@
package dev.yokai.core.migration
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
object Migrator {
private var result: Deferred<Boolean>? = null
val scope = CoroutineScope(Dispatchers.Main + Job())
fun initialize(
old: Int,
new: Int,
migrations: List<Migration>,
dryRun: Boolean = false,
onMigrationComplete: () -> Unit
) {
val migrationContext = MigrationContext(dryRun)
val migrationJobFactory = MigrationJobFactory(migrationContext, scope)
val migrationStrategyFactory = MigrationStrategyFactory(migrationJobFactory, onMigrationComplete)
val strategy = migrationStrategyFactory.create(old, new)
result = strategy(migrations)
}
suspend fun await(): Boolean {
val result = result ?: CompletableDeferred(false)
return result.await()
}
fun release() {
result = null
}
fun awaitAndRelease(): Boolean = runBlocking {
await().also { release() }
}
}

View file

@ -0,0 +1,25 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import java.io.File
/**
* Delete external chapter cache dir.
*/
class ChapterCacheMigration : Migration {
override val version: Float = 26f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val extCache = context.externalCacheDir
if (extCache != null) {
val chapterCache = File(extCache, "chapter_disk_cache")
if (chapterCache.exists()) {
chapterCache.deleteRecursively()
}
}
return true
}
}

View file

@ -0,0 +1,27 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import java.io.File
/**
* Move covers to external files dir.
*/
class CoverCacheMigration : Migration {
override val version: Float = 19f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
if (oldDir.exists()) {
val destDir = context.getExternalFilesDir("covers")
if (destDir != null) {
oldDir.listFiles()?.forEach {
it.renameTo(File(destDir, it.name))
}
}
}
return true
}
}

View file

@ -0,0 +1,18 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
class CustomInfoMigration : Migration {
override val version: Float = 66f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
try {
LibraryPresenter.updateCustoms()
} catch (e: Exception) {
return false
}
return true
}
}

View file

@ -0,0 +1,47 @@
package dev.yokai.core.migration.migrations
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import dev.yokai.domain.ui.settings.ReaderPreferences
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
import dev.yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
class CutoutMigration : Migration {
override val version: Float = 121f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val readerPreferences: ReaderPreferences = migrationContext.get() ?: return false
val context: App = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
try {
val oldCutoutBehaviour = prefs.getInt(PreferenceKeys.pagerCutoutBehavior, 0)
readerPreferences.pagerCutoutBehavior().set(
when (oldCutoutBehaviour) {
PagerConfig.CUTOUT_PAD -> CutoutBehaviour.HIDE
PagerConfig.CUTOUT_IGNORE -> CutoutBehaviour.IGNORE
else -> CutoutBehaviour.SHOW
}
)
} catch (_: Exception) {
readerPreferences.pagerCutoutBehavior().set(CutoutBehaviour.SHOW)
}
try {
val oldCutoutBehaviour = prefs.getInt("landscape_cutout_behavior", 0)
readerPreferences.landscapeCutoutBehavior().set(
when (oldCutoutBehaviour) {
0 -> LandscapeCutoutBehaviour.HIDE
else -> LandscapeCutoutBehaviour.DEFAULT
}
)
} catch (_: Exception) {
readerPreferences.landscapeCutoutBehavior().set(LandscapeCutoutBehaviour.DEFAULT)
}
return true
}
}

View file

@ -0,0 +1,27 @@
package dev.yokai.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
class DoHMigration : Migration {
override val version: Float = 71f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Migrate DNS over HTTPS setting
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
return true
}
}

View file

@ -0,0 +1,16 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.download.DownloadProvider
class DownloadedChaptersMigration : Migration {
override val version: Float = 54f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
DownloadProvider(context).renameChapters()
return true
}
}

View file

@ -0,0 +1,21 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
class EnabledLanguageMigration : Migration {
override val version: Float = 83f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val preferences: PreferencesHelper = migrationContext.get() ?: return false
if (preferences.enabledLanguages().isSet()) {
preferences.enabledLanguages() += "all"
}
return true
}
}

View file

@ -0,0 +1,24 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
/**
* Restore jobs after upgrading to evernote's job scheduler.
*/
class EvernoteJobUpgradeMigration : Migration {
override val version: Float = 14f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
if (BuildConfig.INCLUDE_UPDATER) {
AppUpdateJob.setupTask(context)
}
LibraryUpdateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,34 @@
package dev.yokai.core.migration.migrations
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import dev.yokai.domain.base.BasePreferences
import eu.kanade.tachiyomi.App
/**
* Upstream no longer use Int for extension installer prefs, this solves incompatibility with upstreams backup
*/
class ExtensionInstallerEnumMigration : Migration {
override val version: Float = 119f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val basePreferences: BasePreferences = migrationContext.get() ?: return false
val context: App = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
try {
val oldExtensionInstall = prefs.getInt("extension_installer", 0)
basePreferences.extensionInstaller().set(
when (oldExtensionInstall) {
1 -> BasePreferences.ExtensionInstaller.SHIZUKU
2 -> BasePreferences.ExtensionInstaller.PRIVATE
else -> BasePreferences.ExtensionInstaller.PACKAGEINSTALLER
}
)
} catch (_: Exception) {
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.PACKAGEINSTALLER)
}
return true
}
}

View file

@ -0,0 +1,19 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import java.io.File
/**
* Delete internal chapter cache dir.
*/
class InternalChapterCacheUpdateMigration : Migration {
override val version: Float = 15f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
return true
}
}

View file

@ -0,0 +1,32 @@
package dev.yokai.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.ui.library.LibrarySort
class LibrarySortMigration : Migration {
override val version: Float = 110f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
try {
val librarySortString = prefs.getString("library_sorting_mode", "")
if (!librarySortString.isNullOrEmpty()) {
prefs.edit {
remove("library_sorting_mode")
putInt(
"library_sorting_mode",
LibrarySort.deserialize(librarySortString).mainValue,
)
}
}
} catch (_: Exception) {
}
return true
}
}

View file

@ -0,0 +1,17 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
class LibraryUpdateResetMigration : Migration {
override val version: Float = 105f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
LibraryUpdateJob.cancelAllWorks(context)
LibraryUpdateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,39 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
val migrations: ImmutableList<Migration> = persistentListOf(
SetupAppUpdateMigration(),
SetupBackupCreateMigration(),
SetupExtensionUpdateMigration(),
SetupLibraryUpdateMigration(),
// For archive purposes
EvernoteJobUpgradeMigration(),
InternalChapterCacheUpdateMigration(),
CoverCacheMigration(),
ChapterCacheMigration(),
DownloadedChaptersMigration(),
WorkManagerMigration(),
CustomInfoMigration(),
MyAnimeListMigration(),
DoHMigration(),
RotationTypeMigration(),
ShortcutsMigration(),
RotationTypeEnumMigration(),
EnabledLanguageMigration(),
UpdateIntervalMigration(),
ReaderUpdateMigration(),
PrefsMigration(),
LibraryUpdateResetMigration(),
TrackerPrivateSettingsMigration(),
LibrarySortMigration(),
// Yokai fork
ThePurgeMigration(),
ExtensionInstallerEnumMigration(),
CutoutMigration(),
RepoJsonMigration(),
)

View file

@ -0,0 +1,28 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.system.toast
/**
* Force MAL log out due to login flow change
* v67: switched from scraping to WebView
* v68: switched from WebView to OAuth
*/
class MyAnimeListMigration : Migration {
override val version: Float = 68f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val trackManager: TrackManager = migrationContext.get() ?: return false
val context: App = migrationContext.get() ?: return false
if (trackManager.myAnimeList.isLogged) {
trackManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin)
}
return true
}
}

View file

@ -0,0 +1,37 @@
package dev.yokai.core.migration.migrations
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import kotlin.math.max
class PrefsMigration : Migration {
override val version: Float = 102f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val preferences: PreferencesHelper = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
if (oldSecureScreen) {
preferences.secureScreen().set(PreferenceValues.SecureScreenMode.ALWAYS)
}
val oldDLAfterReading = prefs.getInt("auto_download_after_reading", 0)
if (oldDLAfterReading > 0) {
preferences.autoDownloadWhileReading().set(max(2, oldDLAfterReading))
}
val oldGroupHistory = prefs.getBoolean("group_chapters_history", true)
if (!oldGroupHistory) {
preferences.groupChaptersHistory().set(RecentsPresenter.GroupType.Never)
}
return true
}
}

View file

@ -0,0 +1,29 @@
package dev.yokai.core.migration.migrations
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.util.system.withIOContext
class ReaderUpdateMigration : Migration {
override val version: Float = 88f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val preferences: PreferencesHelper = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
withIOContext {
LibraryPresenter.updateRatiosAndColors()
}
val oldReaderTap = prefs.getBoolean("reader_tap", true)
if (!oldReaderTap) {
preferences.navigationModePager().set(5)
preferences.navigationModeWebtoon().set(5)
}
return true
}
}

View file

@ -0,0 +1,37 @@
package dev.yokai.core.migration.migrations
import co.touchlab.kermit.Logger
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
import dev.yokai.domain.extension.repo.exception.SaveExtensionRepoException
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.util.system.withIOContext
class RepoJsonMigration : Migration {
override val version: Float = 130f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val extensionRepoRepository: ExtensionRepoRepository = migrationContext.get() ?: return@withIOContext false
val preferenceStore: PreferenceStore = migrationContext.get() ?: return@withIOContext false
val extensionRepos: Preference<Set<String>> = preferenceStore.getStringSet("extension_repos", emptySet())
for ((index, source) in extensionRepos.get().withIndex()) {
try {
extensionRepoRepository.upsertRepository(
source,
"Repo #${index + 1}",
null,
source,
"NOFINGERPRINT-${index + 1}",
)
} catch (e: SaveExtensionRepoException) {
Logger.e(e) { "Error Migrating Extension Repo with baseUrl: $source" }
}
}
extensionRepos.delete()
return@withIOContext true
}
}

View file

@ -0,0 +1,38 @@
package dev.yokai.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
class RotationTypeEnumMigration : Migration {
override val version: Float = 77f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue
3 -> OrientationType.LANDSCAPE.flagValue
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
else -> OrientationType.FREE.flagValue
}
// Reading mode flag and prefValue is the same value
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
prefs.edit {
putInt("pref_default_orientation_type_key", newOrientation)
remove("pref_rotation_type_key")
putInt("pref_default_reading_mode_key", newReadingMode)
remove("pref_default_viewer_key")
}
return true
}
}

View file

@ -0,0 +1,22 @@
package dev.yokai.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
class RotationTypeMigration : Migration {
override val version: Float = 73f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
}
}
return true
}
}

View file

@ -0,0 +1,19 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
class SetupAppUpdateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
if (!BuildConfig.INCLUDE_UPDATER) return false
val context: App = migrationContext.get() ?: return false
AppUpdateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,16 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.backup.create.BackupCreatorJob
class SetupBackupCreateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
BackupCreatorJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,16 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
class SetupExtensionUpdateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
ExtensionUpdateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,16 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
class SetupLibraryUpdateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
LibraryUpdateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,36 @@
package dev.yokai.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
class ShortcutsMigration : Migration {
override val version: Float = 75f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val preferences: PreferencesHelper = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasShortcutsDisabled = !prefs.getBoolean("show_manga_app_shortcuts", true)
if (wasShortcutsDisabled) {
prefs.edit {
putBoolean(PreferenceKeys.showSourcesInShortcuts, false)
putBoolean(PreferenceKeys.showSeriesInShortcuts, false)
remove("show_manga_app_shortcuts")
}
}
// Handle removed every 1 or 2 hour library updates
val updateInterval = preferences.libraryUpdateInterval().get()
if (updateInterval == 1 || updateInterval == 2) {
preferences.libraryUpdateInterval().set(3)
LibraryUpdateJob.setupTask(context, 3)
}
return true
}
}

View file

@ -0,0 +1,21 @@
package dev.yokai.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
class ThePurgeMigration : Migration {
override val version: Float = 112f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit {
remove("trusted_signatures")
}
return true
}
}

View file

@ -0,0 +1,26 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
class TrackerPrivateSettingsMigration : Migration {
override val version: Float = 108f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val preferenceStore: PreferenceStore = migrationContext.get() ?: return false
preferenceStore.getAll()
.filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }
.forEach { (key, value) ->
if (value is String) {
preferenceStore
.getString(Preference.privateKey(key))
.set(value)
preferenceStore.getString(key).delete()
}
}
return true
}
}

View file

@ -0,0 +1,24 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
class UpdateIntervalMigration : Migration {
override val version: Float = 86f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
val preferences: PreferencesHelper = migrationContext.get() ?: return false
// Handle removed every 3, 4, 6, and 8 hour library updates
val updateInterval = preferences.libraryUpdateInterval().get()
if (updateInterval in listOf(3, 4, 6, 8)) {
preferences.libraryUpdateInterval().set(12)
LibraryUpdateJob.setupTask(context, 12)
}
return true
}
}

View file

@ -0,0 +1,30 @@
package dev.yokai.core.migration.migrations
import dev.yokai.core.migration.Migration
import dev.yokai.core.migration.MigrationContext
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.create.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
/**
* Restore jobs after migrating from Evernote's job scheduler to WorkManager.
*/
class WorkManagerMigration : Migration {
override val version: Float = 62f
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context: App = migrationContext.get() ?: return false
LibraryPresenter.updateDB()
if (BuildConfig.INCLUDE_UPDATER) {
AppUpdateJob.setupTask(context)
}
LibraryUpdateJob.setupTask(context)
BackupCreatorJob.setupTask(context)
ExtensionUpdateJob.setupTask(context)
return true
}
}

View file

@ -38,8 +38,12 @@ import dev.yokai.core.CrashlyticsLogWriter
import dev.yokai.core.di.AppModule
import dev.yokai.core.di.DomainModule
import dev.yokai.core.di.PreferenceModule
import dev.yokai.core.migration.Migrator
import dev.yokai.core.migration.migrations.migrations
import dev.yokai.domain.base.BasePreferences
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.CoilDiskCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
@ -154,6 +158,34 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
}
}
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
initializeMigrator()
}
private fun initializeMigrator() {
val preferenceStore = Injekt.get<PreferenceStore>()
val preference = preferenceStore.getInt(
Preference.appStateKey("last_version_code"),
0,
)
// TODO: Remove later
val old = preferenceStore.getInt("last_version_code", -1)
if (old.get() >= preference.get()) {
preference.set(old.get())
old.delete()
}
Logger.i { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
Migrator.initialize(
old = preference.get(),
new = BuildConfig.VERSION_CODE,
migrations = migrations,
onMigrationComplete = {
Logger.i { "Updating last version to ${BuildConfig.VERSION_CODE}" }
preference.set(BuildConfig.VERSION_CODE)
},
)
}
override fun onPause(owner: LifecycleOwner) {

View file

@ -1,342 +0,0 @@
package eu.kanade.tachiyomi
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import co.touchlab.kermit.Logger
import dev.yokai.domain.base.BasePreferences
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
import dev.yokai.domain.extension.repo.exception.SaveExtensionRepoException
import dev.yokai.domain.ui.settings.ReaderPreferences
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
import dev.yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.preference.plusAssign
import eu.kanade.tachiyomi.data.backup.create.BackupCreatorJob
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import kotlin.math.max
object Migrations {
/**
* Performs a migration when the application is updated.
*
* @param preferences Preferences of the application.
* @return true if a migration is performed, false otherwise.
*/
fun upgrade(
preferences: PreferencesHelper,
preferenceStore: PreferenceStore,
scope: CoroutineScope,
): Boolean {
val context = preferences.context
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit {
remove(AppDownloadInstallJob.NOTIFY_ON_INSTALL_KEY)
}
val oldVersion = preferences.lastVersionCode().get()
if (oldVersion < BuildConfig.VERSION_CODE) {
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
// Always set up background tasks to ensure they're running
if (BuildConfig.INCLUDE_UPDATER) {
AppUpdateJob.setupTask(context)
}
ExtensionUpdateJob.setupTask(context)
LibraryUpdateJob.setupTask(context)
BackupCreatorJob.setupTask(context)
if (oldVersion == 0) {
return BuildConfig.DEBUG
}
if (oldVersion < 14) {
// Restore jobs after upgrading to evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER) {
AppUpdateJob.setupTask(context)
}
LibraryUpdateJob.setupTask(context)
}
if (oldVersion < 15) {
// Delete internal chapter cache dir.
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
}
if (oldVersion < 19) {
// Move covers to external files dir.
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
if (oldDir.exists()) {
val destDir = context.getExternalFilesDir("covers")
if (destDir != null) {
oldDir.listFiles()?.forEach {
it.renameTo(File(destDir, it.name))
}
}
}
}
if (oldVersion < 26) {
// Delete external chapter cache dir.
val extCache = context.externalCacheDir
if (extCache != null) {
val chapterCache = File(extCache, "chapter_disk_cache")
if (chapterCache.exists()) {
chapterCache.deleteRecursively()
}
}
}
if (oldVersion < 54) {
DownloadProvider(context).renameChapters()
}
if (oldVersion < 62) {
LibraryPresenter.updateDB()
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
if (BuildConfig.INCLUDE_UPDATER) {
AppUpdateJob.setupTask(context)
}
LibraryUpdateJob.setupTask(context)
BackupCreatorJob.setupTask(context)
ExtensionUpdateJob.setupTask(context)
}
if (oldVersion < 66) {
LibraryPresenter.updateCustoms()
}
if (oldVersion < 68) {
// Force MAL log out due to login flow change
// v67: switched from scraping to WebView
// v68: switched from WebView to OAuth
val trackManager = Injekt.get<TrackManager>()
if (trackManager.myAnimeList.isLogged) {
trackManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin)
}
}
if (oldVersion < 71) {
// Migrate DNS over HTTPS setting
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
}
if (oldVersion < 73) {
// Reset rotation to Free after replacing Lock
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
}
}
}
if (oldVersion < 74) {
// Turn on auto updates for all users
if (BuildConfig.INCLUDE_UPDATER) {
AppUpdateJob.setupTask(context)
}
}
if (oldVersion < 75) {
val wasShortcutsDisabled = !prefs.getBoolean("show_manga_app_shortcuts", true)
if (wasShortcutsDisabled) {
prefs.edit {
putBoolean(PreferenceKeys.showSourcesInShortcuts, false)
putBoolean(PreferenceKeys.showSeriesInShortcuts, false)
remove("show_manga_app_shortcuts")
}
}
// Handle removed every 1 or 2 hour library updates
val updateInterval = preferences.libraryUpdateInterval().get()
if (updateInterval == 1 || updateInterval == 2) {
preferences.libraryUpdateInterval().set(3)
LibraryUpdateJob.setupTask(context, 3)
}
}
if (oldVersion < 77) {
// Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue
3 -> OrientationType.LANDSCAPE.flagValue
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
else -> OrientationType.FREE.flagValue
}
// Reading mode flag and prefValue is the same value
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
prefs.edit {
putInt("pref_default_orientation_type_key", newOrientation)
remove("pref_rotation_type_key")
putInt("pref_default_reading_mode_key", newReadingMode)
remove("pref_default_viewer_key")
}
}
if (oldVersion < 83) {
if (preferences.enabledLanguages().isSet()) {
preferences.enabledLanguages() += "all"
}
}
if (oldVersion < 86) {
// Handle removed every 3, 4, 6, and 8 hour library updates
val updateInterval = preferences.libraryUpdateInterval().get()
if (updateInterval in listOf(3, 4, 6, 8)) {
preferences.libraryUpdateInterval().set(12)
LibraryUpdateJob.setupTask(context, 12)
}
}
if (oldVersion < 88) {
scope.launchIO {
LibraryPresenter.updateRatiosAndColors()
}
val oldReaderTap = prefs.getBoolean("reader_tap", true)
if (!oldReaderTap) {
preferences.navigationModePager().set(5)
preferences.navigationModeWebtoon().set(5)
}
}
if (oldVersion < 90) {
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
if (oldSecureScreen) {
preferences.secureScreen().set(PreferenceValues.SecureScreenMode.ALWAYS)
}
}
if (oldVersion < 97) {
val oldDLAfterReading = prefs.getInt("auto_download_after_reading", 0)
if (oldDLAfterReading > 0) {
preferences.autoDownloadWhileReading().set(max(2, oldDLAfterReading))
}
}
if (oldVersion < 102) {
val oldGroupHistory = prefs.getBoolean("group_chapters_history", true)
if (!oldGroupHistory) {
preferences.groupChaptersHistory().set(RecentsPresenter.GroupType.Never)
}
}
if (oldVersion < 105) {
LibraryUpdateJob.cancelAllWorks(context)
LibraryUpdateJob.setupTask(context)
}
if (oldVersion < 108) {
preferenceStore.getAll()
.filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }
.forEach { (key, value) ->
if (value is String) {
preferenceStore
.getString(Preference.privateKey(key))
.set(value)
preferenceStore.getString(key).delete()
}
}
}
if (oldVersion < 110) {
try {
val librarySortString = prefs.getString("library_sorting_mode", "")
if (!librarySortString.isNullOrEmpty()) {
prefs.edit {
remove("library_sorting_mode")
putInt(
"library_sorting_mode",
LibrarySort.deserialize(librarySortString).mainValue,
)
}
}
} catch (_: Exception) {
}
}
if (oldVersion < 112) {
prefs.edit {
remove("trusted_signatures")
}
}
if (oldVersion < 119) {
val basePreferences: BasePreferences = Injekt.get()
try {
val oldExtensionInstall = prefs.getInt("extension_installer", 0)
basePreferences.extensionInstaller().set(
when (oldExtensionInstall) {
1 -> BasePreferences.ExtensionInstaller.SHIZUKU
2 -> BasePreferences.ExtensionInstaller.PRIVATE
else -> BasePreferences.ExtensionInstaller.PACKAGEINSTALLER
}
)
} catch (_: Exception) {
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.PACKAGEINSTALLER)
}
}
if (oldVersion < 121) {
val readerPreferences: ReaderPreferences = Injekt.get()
try {
val oldCutoutBehaviour = prefs.getInt(PreferenceKeys.pagerCutoutBehavior, 0)
readerPreferences.pagerCutoutBehavior().set(
when (oldCutoutBehaviour) {
PagerConfig.CUTOUT_PAD -> CutoutBehaviour.HIDE
PagerConfig.CUTOUT_IGNORE -> CutoutBehaviour.IGNORE
else -> CutoutBehaviour.SHOW
}
)
} catch (_: Exception) {
readerPreferences.pagerCutoutBehavior().set(CutoutBehaviour.SHOW)
}
try {
val oldCutoutBehaviour = prefs.getInt("landscape_cutout_behavior", 0)
readerPreferences.landscapeCutoutBehavior().set(
when (oldCutoutBehaviour) {
0 -> LandscapeCutoutBehaviour.HIDE
else -> LandscapeCutoutBehaviour.DEFAULT
}
)
} catch (_: Exception) {
readerPreferences.landscapeCutoutBehavior().set(LandscapeCutoutBehaviour.DEFAULT)
}
}
if (oldVersion < 130) {
val coroutineScope = CoroutineScope(Dispatchers.IO)
val extensionRepoRepository: ExtensionRepoRepository by injectLazy()
val extensionRepos: Preference<Set<String>> = preferenceStore.getStringSet("extension_repos", emptySet())
coroutineScope.launchIO {
for ((index, source) in extensionRepos.get().withIndex()) {
try {
extensionRepoRepository.upsertRepository(
source,
"Repo #${index + 1}",
null,
source,
"NOFINGERPRINT-${index + 1}",
)
} catch (e: SaveExtensionRepoException) {
Logger.e(e) { "Error Migrating Extension Repo with baseUrl: $source" }
}
}
extensionRepos.delete()
}
}
return true
}
return false
}
}

View file

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.jobIsRunning
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.toInt
@ -38,7 +39,7 @@ import rikka.shizuku.Shizuku
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
import java.util.concurrent.*
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
@ -179,6 +180,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
fun setupTask(context: Context, forceAutoUpdateJob: Boolean? = null) {
val preferences = Injekt.get<PreferencesHelper>()
val autoUpdateJob = forceAutoUpdateJob ?: preferences.automaticExtUpdates().get()
WorkManager.getInstance(context).jobIsRunning(TAG)
if (autoUpdateJob) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)

View file

@ -67,11 +67,11 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import com.google.common.primitives.Floats.max
import com.google.common.primitives.Ints.max
import dev.yokai.core.migration.Migrator
import dev.yokai.domain.base.BasePreferences
import dev.yokai.presentation.extension.repo.ExtensionRepoController
import dev.yokai.presentation.onboarding.OnboardingController
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadJob
@ -142,9 +142,22 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.collections.List
import kotlin.collections.MutableList
import kotlin.collections.MutableMap
import kotlin.collections.distinct
import kotlin.collections.filterNotNull
import kotlin.collections.firstOrNull
import kotlin.collections.forEach
import kotlin.collections.forEachIndexed
import kotlin.collections.lastOrNull
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.maxByOrNull
import kotlin.collections.orEmpty
import kotlin.collections.plus
import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToLong
@ -442,8 +455,10 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
// Reset Incognito Mode on relaunch
preferences.incognitoMode().set(false)
val didMigration = Migrator.awaitAndRelease()
// Show changelog if needed
if (Migrations.upgrade(preferences, Injekt.get(), lifecycleScope)) {
if (didMigration) {
if (!BuildConfig.DEBUG) {
content.post {
whatsNewSheet().show()