Skip to content

Commit

Permalink
Migrator improvements (#588)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghostbear authored Mar 28, 2024
1 parent 666d6aa commit 0265c16
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 114 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ dependencies {
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation(libs.leakcanary.android)
implementation(libs.leakcanary.plumber)

testImplementation(kotlinx.coroutines.test)
}

androidComponents {
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ import kotlinx.coroutines.flow.onEach
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import org.conscrypt.Conscrypt
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager
Expand Down Expand Up @@ -131,6 +135,23 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}

initializeMigrator()
}

private fun initializeMigrator() {
val preferenceStore = Injekt.get<PreferenceStore>()
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
Migrator.initialize(
old = preference.get(),
new = BuildConfig.VERSION_CODE,
migrations = migrations,
onMigrationComplete = {
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
preference.set(BuildConfig.VERSION_CODE)
},
)
}

override fun newImageLoader(context: Context): ImageLoader {
Expand Down
22 changes: 1 addition & 21 deletions app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import tachiyomi.core.common.Constants
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
Expand All @@ -99,8 +96,6 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import androidx.compose.ui.graphics.Color.Companion as ComposeColor

Expand Down Expand Up @@ -129,7 +124,7 @@ class MainActivity : BaseActivity() {

super.onCreate(savedInstanceState)

val didMigration = migrate()
val didMigration = Migrator.awaitAndRelease()

// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
if (!isTaskRoot) {
Expand Down Expand Up @@ -340,21 +335,6 @@ class MainActivity : BaseActivity() {
}
}

private fun migrate(): Boolean {
val preferenceStore = Injekt.get<PreferenceStore>()
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
return Migrator.migrate(
old = preference.get(),
new = BuildConfig.VERSION_CODE,
migrations = migrations,
onMigrationComplete = {
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
preference.set(BuildConfig.VERSION_CODE)
},
)
}

/**
* Sets custom splash screen exit animation on devices prior to Android 12.
*
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/mihon/core/migration/Migration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ interface Migration {

suspend operator fun invoke(migrationContext: MigrationContext): Boolean

val isAlways: Boolean
get() = version == ALWAYS

companion object {
const val ALWAYS = -1f

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package mihon.core.migration

typealias MigrationCompletedListener = () -> Unit
2 changes: 1 addition & 1 deletion app/src/main/java/mihon/core/migration/MigrationContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package mihon.core.migration

import uy.kohesive.injekt.Injekt

class MigrationContext {
class MigrationContext(val dryrun: Boolean) {

inline fun <reified T> get(): T? {
return Injekt.getInstanceOrNull(T::class.java)
Expand Down
30 changes: 30 additions & 0 deletions app/src/main/java/mihon/core/migration/MigrationJobFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package mihon.core.migration

import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import tachiyomi.core.common.util.system.logcat

class MigrationJobFactory(
private val migrationContext: MigrationContext,
private val scope: CoroutineScope
) {

@SuppressWarnings("MaxLineLength")
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) {
logcat { "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
async {
val prev = acc.await()
migration(migrationContext) || prev
}
} else {
logcat { "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
CompletableDeferred(true)
}
}
}
}
55 changes: 55 additions & 0 deletions app/src/main/java/mihon/core/migration/MigrationStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package mihon.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 })
}
}
23 changes: 23 additions & 0 deletions app/src/main/java/mihon/core/migration/MigrationStrategyFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package mihon.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
}
}
60 changes: 24 additions & 36 deletions app/src/main/java/mihon/core/migration/Migrator.kt
Original file line number Diff line number Diff line change
@@ -1,53 +1,41 @@
package mihon.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
import tachiyomi.core.common.util.system.logcat

object Migrator {

@SuppressWarnings("ReturnCount")
fun migrate(
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
): Boolean {
val migrationContext = MigrationContext()

if (old == 0) {
return migrationContext.migrate(
migrations = migrations.filter { it.isAlways() },
dryrun = dryrun
)
.also { onMigrationComplete() }
}

if (old >= new) {
return false
}
) {
val migrationContext = MigrationContext(dryrun)
val migrationJobFactory = MigrationJobFactory(migrationContext, scope)
val migrationStrategyFactory = MigrationStrategyFactory(migrationJobFactory, onMigrationComplete)
val strategy = migrationStrategyFactory.create(old, new)
result = strategy(migrations)
}

return migrationContext.migrate(
migrations = migrations.filter { it.isAlways() || it.version.toInt() in (old + 1)..new },
dryrun = dryrun
)
.also { onMigrationComplete() }
suspend fun await(): Boolean {
val result = result ?: CompletableDeferred(false)
return result.await()
}

private fun Migration.isAlways() = version == Migration.ALWAYS
fun release() {
result = null
}

@SuppressWarnings("MaxLineLength")
private fun MigrationContext.migrate(migrations: List<Migration>, dryrun: Boolean): Boolean {
return migrations.sortedBy { it.version }
.map { migration ->
if (!dryrun) {
logcat { "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
runBlocking { migration(this@migrate) }
} else {
logcat { "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
true
}
}
.reduce { acc, b -> acc || b }
fun awaitAndRelease(): Boolean = runBlocking {
await().also { release() }
}
}
Loading

0 comments on commit 0265c16

Please sign in to comment.