First Commit
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.migration.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
interface MigrationEntryPoint {
|
||||
@Composable
|
||||
fun present(): MigrationState
|
||||
|
||||
@Composable
|
||||
fun Render(
|
||||
state: MigrationState,
|
||||
modifier: Modifier,
|
||||
)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.api
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class MigrationState(
|
||||
val migrationAction: AsyncData<Unit>,
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.migration.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.announcement.api)
|
||||
implementation(projects.features.migration.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.preferences.impl)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.features.announcement.test)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.api.MigrationEntryPoint
|
||||
import io.element.android.features.api.MigrationState
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMigrationEntryPoint(
|
||||
private val migrationPresenter: MigrationPresenter,
|
||||
) : MigrationEntryPoint {
|
||||
@Composable
|
||||
override fun present(): MigrationState = migrationPresenter.present()
|
||||
|
||||
@Composable
|
||||
override fun Render(
|
||||
state: MigrationState,
|
||||
modifier: Modifier,
|
||||
) = MigrationView(
|
||||
migrationState = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMigrationStore(
|
||||
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||
) : MigrationStore {
|
||||
private val store = preferenceDataStoreFactory.create("elementx_migration")
|
||||
|
||||
override suspend fun setApplicationMigrationVersion(version: Int) {
|
||||
store.edit { prefs ->
|
||||
prefs[applicationMigrationVersion] = version
|
||||
}
|
||||
}
|
||||
|
||||
override fun applicationMigrationVersion(): Flow<Int> {
|
||||
return store.data.map { prefs ->
|
||||
prefs[applicationMigrationVersion] ?: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.api.MigrationState
|
||||
import io.element.android.features.migration.impl.migrations.AppMigration
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import timber.log.Timber
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@Inject
|
||||
class MigrationPresenter(
|
||||
private val migrationStore: MigrationStore,
|
||||
migrations: Set<@JvmSuppressWildcards AppMigration>,
|
||||
) : 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 {
|
||||
val migrationStoreVersion by remember {
|
||||
migrationStore.applicationMigrationVersion()
|
||||
}.collectAsState(initial = null)
|
||||
var migrationAction: AsyncData<Unit> by remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
|
||||
// Uncomment this block to run the migration everytime
|
||||
// LaunchedEffect(Unit) {
|
||||
// Timber.d("Resetting migration version to 0")
|
||||
// migrationStore.setApplicationMigrationVersion(0)
|
||||
// }
|
||||
|
||||
LaunchedEffect(migrationStoreVersion) {
|
||||
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.")
|
||||
migrationAction = AsyncData.Success(Unit)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
migrationAction = AsyncData.Loading(Unit)
|
||||
val nextMigration = orderedMigrations.firstOrNull { it.order > migrationValue }
|
||||
if (nextMigration != null) {
|
||||
Timber.d("Current app migration version: $migrationValue. Applying migration: ${nextMigration.order}")
|
||||
nextMigration.migrate(isFreshInstall)
|
||||
migrationStore.setApplicationMigrationVersion(nextMigration.order)
|
||||
}
|
||||
}
|
||||
|
||||
return MigrationState(
|
||||
migrationAction = migrationAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.api.MigrationState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
internal class MigrationStateProvider : PreviewParameterProvider<MigrationState> {
|
||||
override val values: Sequence<MigrationState>
|
||||
get() = sequenceOf(
|
||||
aMigrationState(),
|
||||
aMigrationState(migrationAction = AsyncData.Loading(Unit)),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aMigrationState(
|
||||
migrationAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||
) = MigrationState(
|
||||
migrationAction = migrationAction,
|
||||
)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface MigrationStore {
|
||||
/**
|
||||
* Return of flow of the current value for application migration version.
|
||||
* If the value is not set, it will emit 0.
|
||||
* If the emitted value is lower than the current application migration version, it means
|
||||
* that a migration should occur, and at the end [setApplicationMigrationVersion] should be called.
|
||||
*/
|
||||
fun applicationMigrationVersion(): Flow<Int>
|
||||
|
||||
/**
|
||||
* Set the application migration version, typically after a migration has been done.
|
||||
*/
|
||||
suspend fun setApplicationMigrationVersion(version: Int)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.api.MigrationState
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun MigrationView(
|
||||
migrationState: MigrationState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
if (migrationState.migrationAction.isLoading()) {
|
||||
Text(text = stringResource(id = CommonStrings.common_please_wait))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MigrationViewPreview(
|
||||
@PreviewParameter(MigrationStateProvider::class) state: MigrationState,
|
||||
) = ElementPreview {
|
||||
MigrationView(
|
||||
migrationState = state,
|
||||
)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
interface AppMigration {
|
||||
val order: Int
|
||||
suspend fun migrate(isFreshInstall: Boolean)
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||
|
||||
/**
|
||||
* Remove existing logs from the device to remove any leaks of sensitive data.
|
||||
*/
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration01(
|
||||
private val logFilesRemover: LogFilesRemover,
|
||||
) : AppMigration {
|
||||
override val order: Int = 1
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
logFilesRemover.perform()
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
/**
|
||||
* This migration sets the skip session verification preference to true for all existing sessions.
|
||||
* This way we don't force existing users to verify their session again.
|
||||
*/
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration02(
|
||||
private val sessionStore: SessionStore,
|
||||
private val sessionPreferenceStoreFactory: SessionPreferencesStoreFactory,
|
||||
) : AppMigration {
|
||||
override val order: Int = 2
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
coroutineScope {
|
||||
for (session in sessionStore.getAllSessions()) {
|
||||
val sessionId = SessionId(session.userId)
|
||||
val preferences = sessionPreferenceStoreFactory.get(sessionId, this)
|
||||
preferences.setSkipSessionVerification(true)
|
||||
// This session preference store must be ephemeral since it's not created with the right coroutine scope
|
||||
sessionPreferenceStoreFactory.remove(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
/**
|
||||
* This performs the same operation as [AppMigration01], since we need to clear the local logs again.
|
||||
*/
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration03(
|
||||
private val migration01: AppMigration01,
|
||||
) : AppMigration {
|
||||
override val order: Int = 3
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
migration01.migrate(isFreshInstall)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
/**
|
||||
* Remove notifications.bin file, used to store notification data locally.
|
||||
*/
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration04(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : AppMigration {
|
||||
companion object {
|
||||
internal const val NOTIFICATION_FILE_NAME = "notifications.bin"
|
||||
}
|
||||
override val order: Int = 4
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
runCatchingExceptions { context.getDatabasePath(NOTIFICATION_FILE_NAME).delete() }
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.BaseDirectory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import java.io.File
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration05(
|
||||
private val sessionStore: SessionStore,
|
||||
@BaseDirectory private val baseDirectory: File,
|
||||
) : AppMigration {
|
||||
override val order: Int = 5
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
val allSessions = sessionStore.getAllSessions()
|
||||
for (session in allSessions) {
|
||||
if (session.sessionPath.isEmpty()) {
|
||||
val sessionPath = File(baseDirectory, session.userId.replace(':', '_')).absolutePath
|
||||
sessionStore.updateData(session.copy(sessionPath = sessionPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Create the cache directory for the existing sessions.
|
||||
*/
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration06(
|
||||
private val sessionStore: SessionStore,
|
||||
@CacheDirectory private val cacheDirectory: File,
|
||||
) : AppMigration {
|
||||
override val order: Int = 6
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
val allSessions = sessionStore.getAllSessions()
|
||||
for (session in allSessions) {
|
||||
if (session.cachePath.isEmpty()) {
|
||||
val sessionFile = File(session.sessionPath)
|
||||
val sessionFolder = sessionFile.name
|
||||
val cachePath = File(cacheDirectory, sessionFolder)
|
||||
sessionStore.updateData(session.copy(cachePath = cachePath.absolutePath))
|
||||
// Move existing cache files
|
||||
listOf(
|
||||
"matrix-sdk-event-cache.sqlite3",
|
||||
"matrix-sdk-event-cache.sqlite3-shm",
|
||||
"matrix-sdk-event-cache.sqlite3-wal",
|
||||
).map { fileName ->
|
||||
File(sessionFile, fileName)
|
||||
}.takeIf { files ->
|
||||
files.all { it.exists() }
|
||||
}?.forEach { cacheFile ->
|
||||
val targetFile = File(cachePath, cacheFile.name)
|
||||
cacheFile.copyTo(targetFile)
|
||||
cacheFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||
|
||||
/**
|
||||
* Delete the previous log files.
|
||||
*/
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration07(
|
||||
private val logFilesRemover: LogFilesRemover,
|
||||
) : AppMigration {
|
||||
override val order: Int = 7
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
logFilesRemover.perform { file ->
|
||||
file.name.startsWith("logs-")
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.announcement.api.AnnouncementService
|
||||
|
||||
/**
|
||||
* Ensure the new notification sound banner is displayed, but only on application upgrade.
|
||||
*/
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class AppMigration08(
|
||||
private val announcementService: AnnouncementService,
|
||||
) : AppMigration {
|
||||
override val order: Int = 8
|
||||
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
if (!isFreshInstall) {
|
||||
announcementService.showAnnouncement(Announcement.NewNotificationSound)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class InMemoryMigrationStore(
|
||||
initialApplicationMigrationVersion: Int = 0
|
||||
) : MigrationStore {
|
||||
private val applicationMigrationVersion = MutableStateFlow(initialApplicationMigrationVersion)
|
||||
|
||||
override suspend fun setApplicationMigrationVersion(version: Int) {
|
||||
applicationMigrationVersion.value = version
|
||||
}
|
||||
|
||||
override fun applicationMigrationVersion(): Flow<Int> {
|
||||
return applicationMigrationVersion
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.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
|
||||
import org.junit.Test
|
||||
|
||||
class MigrationPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - run all migrations on fresh installation, and last version should be stored`() = runTest {
|
||||
val migrations = (1..10).map { order ->
|
||||
FakeAppMigration(order = order)
|
||||
}
|
||||
val store = InMemoryMigrationStore(initialApplicationMigrationVersion = -1)
|
||||
val presenter = createPresenter(
|
||||
migrationStore = store,
|
||||
migrations = migrations.toSet(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
|
||||
skipItems(migrations.size)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
|
||||
}
|
||||
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
|
||||
}
|
||||
for (migration in migrations) {
|
||||
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(
|
||||
order = it,
|
||||
migrateLambda = lambdaRecorder<Boolean, Unit> { lambdaError() },
|
||||
)
|
||||
}
|
||||
val store = InMemoryMigrationStore(migrations.maxOf { it.order })
|
||||
val presenter = createPresenter(
|
||||
migrationStore = store,
|
||||
migrations = migrations.toSet(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - testing all migrations`() = runTest {
|
||||
val store = InMemoryMigrationStore(0)
|
||||
val migrations = (1..10).map { FakeAppMigration(it) }
|
||||
val presenter = createPresenter(
|
||||
migrationStore = store,
|
||||
migrations = migrations.toSet(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.migrationAction).isEqualTo(AsyncData.Loading(Unit))
|
||||
}
|
||||
consumeItemsUntilPredicate { it.migrationAction is AsyncData.Success }
|
||||
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
|
||||
for (migration in migrations) {
|
||||
migration.migrateLambda.assertions().isCalledOnce().with(value(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
migrationStore: MigrationStore = InMemoryMigrationStore(0),
|
||||
migrations: Set<AppMigration> = setOf(FakeAppMigration(1)),
|
||||
) = MigrationPresenter(
|
||||
migrationStore = migrationStore,
|
||||
migrations = migrations,
|
||||
)
|
||||
|
||||
private class FakeAppMigration(
|
||||
override val order: Int,
|
||||
val migrateLambda: LambdaOneParamRecorder<Boolean, Unit> = lambdaRecorder<Boolean, Unit> { },
|
||||
) : AppMigration {
|
||||
override suspend fun migrate(isFreshInstall: Boolean) {
|
||||
migrateLambda(isFreshInstall)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AppMigration01Test {
|
||||
@Test
|
||||
fun `test migration`() = runTest {
|
||||
val logsFileRemover = FakeLogFilesRemover()
|
||||
val migration = AppMigration01(logsFileRemover)
|
||||
|
||||
migration.migrate(true)
|
||||
|
||||
logsFileRemover.performLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AppMigration02Test {
|
||||
@Test
|
||||
fun `test migration`() = runTest {
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(aSessionData()),
|
||||
)
|
||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSessionVerificationSkipped = false)
|
||||
val sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(
|
||||
getLambda = lambdaRecorder { _, _ -> sessionPreferencesStore },
|
||||
removeLambda = lambdaRecorder { _ -> }
|
||||
)
|
||||
val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory)
|
||||
|
||||
migration.migrate(true)
|
||||
|
||||
// We got the session preferences store
|
||||
sessionPreferencesStoreFactory.getLambda.assertions().isCalledOnce()
|
||||
// We changed the settings for the skipping the session verification
|
||||
assertThat(sessionPreferencesStore.isSessionVerificationSkipped().first()).isTrue()
|
||||
// We removed the session preferences store from cache
|
||||
sessionPreferencesStoreFactory.removeLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AppMigration03Test {
|
||||
@Test
|
||||
fun `test migration`() = runTest {
|
||||
val logsFileRemover = FakeLogFilesRemover()
|
||||
val migration = AppMigration03(migration01 = AppMigration01(logsFileRemover))
|
||||
|
||||
migration.migrate(true)
|
||||
|
||||
logsFileRemover.performLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AppMigration04Test {
|
||||
@Test
|
||||
fun `test migration`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
// Create fake temporary file at the path to be deleted
|
||||
val file = context.getDatabasePath(AppMigration04.NOTIFICATION_FILE_NAME)
|
||||
file.parentFile?.mkdirs()
|
||||
file.createNewFile()
|
||||
assertThat(file.exists()).isTrue()
|
||||
|
||||
val migration = AppMigration04(context)
|
||||
|
||||
migration.migrate(true)
|
||||
|
||||
// Check that the file has been deleted
|
||||
assertThat(file.exists()).isFalse()
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
class AppMigration05Test {
|
||||
@Test
|
||||
fun `empty session path should be set to an expected path`() = runTest {
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_SESSION_ID.value,
|
||||
sessionPath = "",
|
||||
)
|
||||
)
|
||||
)
|
||||
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
|
||||
migration.migrate(true)
|
||||
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
|
||||
assertThat(storedData.sessionPath).isEqualTo("/a/path/${A_SESSION_ID.value.replace(':', '_')}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non empty session path should not be impacted by the migration`() = runTest {
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_SESSION_ID.value,
|
||||
sessionPath = "/a/path/existing",
|
||||
)
|
||||
)
|
||||
)
|
||||
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
|
||||
migration.migrate(true)
|
||||
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
|
||||
assertThat(storedData.sessionPath).isEqualTo("/a/path/existing")
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
class AppMigration06Test {
|
||||
@Test
|
||||
fun `empty cache path should be set to an expected path`() = runTest {
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_SESSION_ID.value,
|
||||
sessionPath = "/a/path/to/a/session/AN_ID",
|
||||
cachePath = "",
|
||||
)
|
||||
)
|
||||
)
|
||||
val migration = AppMigration06(sessionStore = sessionStore, cacheDirectory = File("/a/path/cache"))
|
||||
migration.migrate(true)
|
||||
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
|
||||
assertThat(storedData.cachePath).isEqualTo("/a/path/cache/AN_ID")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non empty cache path should not be impacted by the migration`() = runTest {
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(
|
||||
sessionId = A_SESSION_ID.value,
|
||||
cachePath = "/a/path/existing",
|
||||
)
|
||||
)
|
||||
)
|
||||
val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path/cache"))
|
||||
migration.migrate(true)
|
||||
val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
|
||||
assertThat(storedData.cachePath).isEqualTo("/a/path/existing")
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
class AppMigration07Test {
|
||||
@Test
|
||||
fun `test migration`() = runTest {
|
||||
val performLambda = lambdaRecorder<(File) -> Boolean, Unit> { predicate ->
|
||||
// Test the predicate
|
||||
assertThat(predicate(File("logs-0433.log.gz"))).isTrue()
|
||||
assertThat(predicate(File("logs.2024-08-01-20.log.gz"))).isFalse()
|
||||
}
|
||||
val logsFileRemover = FakeLogFilesRemover(performLambda = performLambda)
|
||||
val migration = AppMigration07(logsFileRemover)
|
||||
migration.migrate(true)
|
||||
performLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl.migrations
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.announcement.api.Announcement
|
||||
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||
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.Test
|
||||
|
||||
class AppMigration08Test {
|
||||
@Test
|
||||
fun `migration on fresh install should not invoke the AnnouncementService`() = runTest {
|
||||
val service = FakeAnnouncementService(
|
||||
showAnnouncementResult = { lambdaError() },
|
||||
)
|
||||
val migration = AppMigration08(service)
|
||||
migration.migrate(isFreshInstall = true)
|
||||
assertThat(service.announcementsToShowFlow().first()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `migration on upgrade should invoke the AnnouncementService`() = runTest {
|
||||
val showAnnouncementResult = lambdaRecorder<Announcement, Unit> { }
|
||||
val service = FakeAnnouncementService(
|
||||
showAnnouncementResult = showAnnouncementResult,
|
||||
)
|
||||
val migration = AppMigration08(service)
|
||||
migration.migrate(isFreshInstall = false)
|
||||
showAnnouncementResult.assertions().isCalledOnce()
|
||||
.with(value(Announcement.NewNotificationSound))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user