Add isFreshInstall Boolean to allow the miration to behave in a different way for an application upgrade or a fresh install.

We cannot restore the previous code which existed because of https://github.com/element-hq/element-x-android/pull/3535
This commit is contained in:
Benoit Marty
2025-10-07 12:16:49 +02:00
committed by Benoit Marty
parent 2f1866afd8
commit 71d2c1d9df
17 changed files with 35 additions and 26 deletions

View File

@@ -31,6 +31,7 @@ class MigrationPresenter(
) : Presenter<MigrationState> {
private val orderedMigrations = migrations.sortedBy { it.order }
private val lastMigration: Int = orderedMigrations.lastOrNull()?.order ?: 0
private var isFreshInstall = false
@Composable
override fun present(): MigrationState {
@@ -49,6 +50,7 @@ class MigrationPresenter(
val migrationValue = migrationStoreVersion ?: return@LaunchedEffect
if (migrationValue == -1) {
Timber.d("Fresh install, or previous installed application did not have the migration mechanism.")
isFreshInstall = true
}
if (migrationValue == lastMigration) {
Timber.d("Current app migration version: $migrationValue. No migration needed.")
@@ -59,7 +61,7 @@ class MigrationPresenter(
val nextMigration = orderedMigrations.firstOrNull { it.order > migrationValue }
if (nextMigration != null) {
Timber.d("Current app migration version: $migrationValue. Applying migration: ${nextMigration.order}")
nextMigration.migrate()
nextMigration.migrate(isFreshInstall)
migrationStore.setApplicationMigrationVersion(nextMigration.order)
}
}

View File

@@ -9,5 +9,5 @@ package io.element.android.features.migration.impl.migrations
interface AppMigration {
val order: Int
suspend fun migrate()
suspend fun migrate(isFreshInstall: Boolean)
}

View File

@@ -22,7 +22,7 @@ class AppMigration01(
) : AppMigration {
override val order: Int = 1
override suspend fun migrate() {
override suspend fun migrate(isFreshInstall: Boolean) {
logFilesRemover.perform()
}
}

View File

@@ -27,7 +27,7 @@ class AppMigration02(
) : AppMigration {
override val order: Int = 2
override suspend fun migrate() {
override suspend fun migrate(isFreshInstall: Boolean) {
coroutineScope {
for (session in sessionStore.getAllSessions()) {
val sessionId = SessionId(session.userId)

View File

@@ -21,7 +21,7 @@ class AppMigration03(
) : AppMigration {
override val order: Int = 3
override suspend fun migrate() {
migration01.migrate()
override suspend fun migrate(isFreshInstall: Boolean) {
migration01.migrate(isFreshInstall)
}
}

View File

@@ -27,7 +27,7 @@ class AppMigration04(
}
override val order: Int = 4
override suspend fun migrate() {
override suspend fun migrate(isFreshInstall: Boolean) {
runCatchingExceptions { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() }
}
}

View File

@@ -22,7 +22,7 @@ class AppMigration05(
) : AppMigration {
override val order: Int = 5
override suspend fun migrate() {
override suspend fun migrate(isFreshInstall: Boolean) {
val allSessions = sessionStore.getAllSessions()
for (session in allSessions) {
if (session.sessionPath.isEmpty()) {

View File

@@ -25,7 +25,7 @@ class AppMigration06(
) : AppMigration {
override val order: Int = 6
override suspend fun migrate() {
override suspend fun migrate(isFreshInstall: Boolean) {
val allSessions = sessionStore.getAllSessions()
for (session in allSessions) {
if (session.cachePath.isEmpty()) {

View File

@@ -22,7 +22,7 @@ class AppMigration07(
) : AppMigration {
override val order: Int = 7
override suspend fun migrate() {
override suspend fun migrate(isFreshInstall: Boolean) {
logFilesRemover.perform { file ->
file.name.startsWith("logs-")
}

View File

@@ -15,8 +15,10 @@ import io.element.android.features.migration.impl.migrations.AppMigration
import io.element.android.libraries.architecture.AsyncData
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -48,13 +50,18 @@ class MigrationPresenterTest {
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
}
for (migration in migrations) {
migration.migrateLambda.assertions().isCalledOnce()
migration.migrateLambda.assertions().isCalledOnce().with(value(true))
}
}
@Test
fun `present - no migration should occurs if ApplicationMigrationVersion is the last one`() = runTest {
val migrations = (1..10).map { FakeAppMigration(it) }
val migrations = (1..10).map {
FakeAppMigration(
order = it,
migrateLambda = lambdaRecorder<Boolean, Unit> { lambdaError() },
)
}
val store = InMemoryMigrationStore(migrations.maxOf { it.order })
val presenter = createPresenter(
migrationStore = store,
@@ -90,7 +97,7 @@ class MigrationPresenterTest {
consumeItemsUntilPredicate { it.migrationAction is AsyncData.Success }
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
for (migration in migrations) {
migration.migrateLambda.assertions().isCalledOnce()
migration.migrateLambda.assertions().isCalledOnce().with(value(false))
}
}
}
@@ -106,9 +113,9 @@ private fun createPresenter(
private class FakeAppMigration(
override val order: Int,
val migrateLambda: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
val migrateLambda: LambdaOneParamRecorder<Boolean, Unit> = lambdaRecorder<Boolean, Unit> { },
) : AppMigration {
override suspend fun migrate() {
migrateLambda()
override suspend fun migrate(isFreshInstall: Boolean) {
migrateLambda(isFreshInstall)
}
}

View File

@@ -17,7 +17,7 @@ class AppMigration01Test {
val logsFileRemover = FakeLogFilesRemover()
val migration = AppMigration01(logsFileRemover)
migration.migrate()
migration.migrate(true)
logsFileRemover.performLambda.assertions().isCalledOnce()
}

View File

@@ -30,7 +30,7 @@ class AppMigration02Test {
)
val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory)
migration.migrate()
migration.migrate(true)
// We got the session preferences store
sessionPreferencesStoreFactory.getLambda.assertions().isCalledOnce()

View File

@@ -17,7 +17,7 @@ class AppMigration03Test {
val logsFileRemover = FakeLogFilesRemover()
val migration = AppMigration03(migration01 = AppMigration01(logsFileRemover))
migration.migrate()
migration.migrate(true)
logsFileRemover.performLambda.assertions().isCalledOnce()
}

View File

@@ -28,7 +28,7 @@ class AppMigration04Test {
val migration = AppMigration04(context)
migration.migrate()
migration.migrate(true)
// Check that the file has been deleted
assertThat(file.exists()).isFalse()

View File

@@ -27,7 +27,7 @@ class AppMigration05Test {
)
)
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
migration.migrate()
migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.sessionPath).isEqualTo("/a/path/${A_SESSION_ID.value.replace(':', '_')}")
}
@@ -43,7 +43,7 @@ class AppMigration05Test {
)
)
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
migration.migrate()
migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.sessionPath).isEqualTo("/a/path/existing")
}

View File

@@ -28,7 +28,7 @@ class AppMigration06Test {
)
)
val migration = AppMigration06(sessionStore = sessionStore, cacheDirectory = File("/a/path/cache"))
migration.migrate()
migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.cachePath).isEqualTo("/a/path/cache/AN_ID")
}
@@ -44,7 +44,7 @@ class AppMigration06Test {
)
)
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path/cache"))
migration.migrate()
migration.migrate(true)
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
assertThat(storedData.cachePath).isEqualTo("/a/path/existing")
}

View File

@@ -24,7 +24,7 @@ class AppMigration07Test {
}
val logsFileRemover = FakeLogFilesRemover(performLambda = performLambda)
val migration = AppMigration07(logsFileRemover)
migration.migrate()
migration.migrate(true)
performLambda.assertions().isCalledOnce()
}
}