Implement a migration mechanism to handle internal stuff which need to occur during application upgrade.
Remove VectorFileLogger, it was dead code.
This commit is contained in:
committed by
Benoit Marty
parent
5fc95f56ab
commit
f6b5b8419b
27
features/migration/api/build.gradle.kts
Normal file
27
features/migration/api/build.gradle.kts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.migration.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.api
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class MigrationState(
|
||||
val migrationAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||
)
|
||||
44
features/migration/impl/build.gradle.kts
Normal file
44
features/migration/impl/build.gradle.kts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.migration.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.migration.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.api.MigrationEntryPoint
|
||||
import io.element.android.features.api.MigrationState
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMigrationEntryPoint @Inject constructor(
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_migration")
|
||||
private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMigrationStore @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : MigrationStore {
|
||||
private val store = context.dataStore
|
||||
|
||||
override suspend fun setApplicationMigrationVersion(version: Int) {
|
||||
store.edit { prefs ->
|
||||
prefs[applicationMigrationVersion] = version
|
||||
}
|
||||
}
|
||||
|
||||
override fun applicationMigrationVersion(): Flow<Int> {
|
||||
return store.data.map { prefs ->
|
||||
prefs[applicationMigrationVersion] ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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 io.element.android.features.api.MigrationState
|
||||
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
|
||||
class MigrationPresenter @Inject constructor(
|
||||
private val migrationStore: MigrationStore,
|
||||
private val logFilesRemover: LogFilesRemover,
|
||||
) : Presenter<MigrationState> {
|
||||
@Composable
|
||||
override fun present(): MigrationState {
|
||||
val migrationStoreVersion = migrationStore.applicationMigrationVersion().collectAsState(initial = null)
|
||||
var migrationAction: AsyncData<Unit> by remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
|
||||
// Uncomment this block to run the migration everytime
|
||||
/*
|
||||
LaunchedEffect(Unit) {
|
||||
migrationStore.setApplicationMigrationVersion(0)
|
||||
}
|
||||
*/
|
||||
|
||||
LaunchedEffect(migrationStoreVersion.value) {
|
||||
val migrationValue = migrationStoreVersion.value ?: return@LaunchedEffect
|
||||
if (migrationValue == MIGRATION_VERSION) {
|
||||
migrationAction = AsyncData.Success(Unit)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
migrationAction = AsyncData.Loading(Unit)
|
||||
if (migrationValue < 1) {
|
||||
logFilesRemover.perform()
|
||||
}
|
||||
// Add new step here
|
||||
|
||||
migrationStore.setApplicationMigrationVersion(MIGRATION_VERSION)
|
||||
}
|
||||
|
||||
return MigrationState(
|
||||
migrationAction = migrationAction,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Increment this value when you need to run the migration again, and
|
||||
// add step in the LaunchedEffect above
|
||||
const val MIGRATION_VERSION = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface MigrationStore {
|
||||
suspend fun setApplicationMigrationVersion(version: Int)
|
||||
fun applicationMigrationVersion(): Flow<Int>
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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.rageshake.api.logs.LogFilesRemover
|
||||
import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
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 - no migration should occurs if ApplicationMigrationVersion is the last one`() = runTest {
|
||||
val store = InMemoryMigrationStore(MigrationPresenter.MIGRATION_VERSION)
|
||||
val presenter = createPresenter(
|
||||
migrationStore = store,
|
||||
)
|
||||
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 logFilesRemoverLambda = lambdaRecorder { -> }
|
||||
val presenter = createPresenter(
|
||||
migrationStore = store,
|
||||
logFilesRemover = FakeLogFilesRemover(logFilesRemoverLambda),
|
||||
)
|
||||
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))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
|
||||
}
|
||||
logFilesRemoverLambda.assertions().isCalledExactly(1)
|
||||
assertThat(store.applicationMigrationVersion().first()).isEqualTo(MigrationPresenter.MIGRATION_VERSION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
migrationStore: MigrationStore = InMemoryMigrationStore(0),
|
||||
logFilesRemover: LogFilesRemover = FakeLogFilesRemover(lambdaRecorder(ensureNeverCalled = true) { -> }),
|
||||
): MigrationPresenter {
|
||||
return MigrationPresenter(
|
||||
migrationStore = migrationStore,
|
||||
logFilesRemover = logFilesRemover,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user