From f6b5b8419bff19935822fe2ea70d12e46e5ab97b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Apr 2024 11:15:33 +0200 Subject: [PATCH] Implement a migration mechanism to handle internal stuff which need to occur during application upgrade. Remove VectorFileLogger, it was dead code. --- app/build.gradle.kts | 1 + .../io/element/android/x/MainActivity.kt | 39 ++-- .../io/element/android/x/di/AppBindings.kt | 3 + features/migration/api/build.gradle.kts | 27 +++ .../features/api/MigrationEntryPoint.kt | 31 +++ .../android/features/api/MigrationState.kt | 23 +++ features/migration/impl/build.gradle.kts | 44 +++++ .../impl/DefaultMigrationEntryPoint.kt | 42 +++++ .../migration/impl/DefaultMigrationStore.kt | 52 ++++++ .../migration/impl/MigrationPresenter.kt | 73 ++++++++ .../migration/impl/MigrationStateProvider.kt | 35 ++++ .../features/migration/impl/MigrationStore.kt | 24 +++ .../features/migration/impl/MigrationView.kt | 65 +++++++ .../migration/impl/InMemoryMigrationStore.kt | 34 ++++ .../migration/impl/MigrationPresenterTest.kt | 87 +++++++++ .../rageshake/api/logs/LogFilesRemover.kt | 21 +++ .../impl/bugreport/BugReportPresenter.kt | 5 +- .../impl/logs/DefaultLogFilesRemover.kt | 32 ++++ .../rageshake/impl/logs/VectorFileLogger.kt | 176 ------------------ .../impl/reporter/DefaultBugReporter.kt | 7 +- .../impl/bugreport/BugReportPresenterTest.kt | 16 +- .../impl/logs/VectorFileLoggerTest.kt | 58 ------ features/rageshake/test/build.gradle.kts | 1 + .../test/logs/FakeLogFilesRemover.kt | 28 +++ .../src/main/res/values/localazy.xml | 1 + 25 files changed, 671 insertions(+), 254 deletions(-) create mode 100644 features/migration/api/build.gradle.kts create mode 100644 features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationEntryPoint.kt create mode 100644 features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationState.kt create mode 100644 features/migration/impl/build.gradle.kts create mode 100644 features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt create mode 100644 features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt create mode 100644 features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt create mode 100644 features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt create mode 100644 features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStore.kt create mode 100644 features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt create mode 100644 features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/InMemoryMigrationStore.kt create mode 100644 features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt create mode 100644 features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt create mode 100644 features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt delete mode 100644 features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt delete mode 100644 features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt create mode 100644 features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3f34f9ea6..6ecfe66fb6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -219,6 +219,7 @@ dependencies { allServicesImpl() allFeaturesImpl(rootDir, logger) implementation(projects.features.call) + implementation(projects.features.migration.api) implementation(projects.anvilannotations) implementation(projects.appnav) implementation(projects.appconfig) diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index ee277a40cf..6dc1e7fc6a 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -86,6 +86,7 @@ class MainActivity : NodeActivity() { appBindings.preferencesStore().getThemeFlow().mapToTheme() } .collectAsState(initial = Theme.System) + val migrationState = appBindings.migrationEntryPoint().present() ElementTheme( darkTheme = theme.isDark() ) { @@ -98,19 +99,12 @@ class MainActivity : NodeActivity() { .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - NodeHost(integrationPoint = appyxIntegrationPoint) { - MainNode( - it, - plugins = listOf( - object : NodeReadyObserver { - override fun init(node: MainNode) { - Timber.tag(loggerTag.value).w("onMainNodeInit") - mainNode = node - mainNode.handleIntent(intent) - } - } - ), - context = applicationContext + if (migrationState.migrationAction.isSuccess()) { + MainNodeHost() + } else { + appBindings.migrationEntryPoint().Render( + state = migrationState, + modifier = Modifier, ) } } @@ -118,6 +112,25 @@ class MainActivity : NodeActivity() { } } + @Composable + private fun MainNodeHost() { + NodeHost(integrationPoint = appyxIntegrationPoint) { + MainNode( + it, + plugins = listOf( + object : NodeReadyObserver { + override fun init(node: MainNode) { + Timber.tag(loggerTag.value).w("onMainNodeInit") + mainNode = node + mainNode.handleIntent(intent) + } + } + ), + context = applicationContext + ) + } + } + /** * Called when: * - the launcher icon is clicked (if the app is already running); diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index 0934771501..d8be841b97 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -17,6 +17,7 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo +import io.element.android.features.api.MigrationEntryPoint import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.features.rageshake.api.reporter.BugReporter @@ -35,4 +36,6 @@ interface AppBindings { fun lockScreenService(): LockScreenService fun preferencesStore(): AppPreferencesStore + + fun migrationEntryPoint(): MigrationEntryPoint } diff --git a/features/migration/api/build.gradle.kts b/features/migration/api/build.gradle.kts new file mode 100644 index 0000000000..485635259e --- /dev/null +++ b/features/migration/api/build.gradle.kts @@ -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) +} diff --git a/features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationEntryPoint.kt b/features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationEntryPoint.kt new file mode 100644 index 0000000000..bd3ad4c466 --- /dev/null +++ b/features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationEntryPoint.kt @@ -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, + ) +} diff --git a/features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationState.kt b/features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationState.kt new file mode 100644 index 0000000000..d8d4dc1bf9 --- /dev/null +++ b/features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationState.kt @@ -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 = AsyncData.Uninitialized, +) diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts new file mode 100644 index 0000000000..5ae18e8791 --- /dev/null +++ b/features/migration/impl/build.gradle.kts @@ -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) +} diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt new file mode 100644 index 0000000000..866e3cfd65 --- /dev/null +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt @@ -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, + ) +} diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt new file mode 100644 index 0000000000..a0158061e2 --- /dev/null +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt @@ -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 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 { + return store.data.map { prefs -> + prefs[applicationMigrationVersion] ?: 0 + } + } +} diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt new file mode 100644 index 0000000000..2f1b04ded5 --- /dev/null +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt @@ -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 { + @Composable + override fun present(): MigrationState { + val migrationStoreVersion = migrationStore.applicationMigrationVersion().collectAsState(initial = null) + var migrationAction: AsyncData 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 + } +} diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt new file mode 100644 index 0000000000..a2729c93f3 --- /dev/null +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt @@ -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 { + override val values: Sequence + get() = sequenceOf( + aMigrationState(), + aMigrationState(migrationAction = AsyncData.Loading(Unit)), + ) +} + +internal fun aMigrationState( + migrationAction: AsyncData = AsyncData.Uninitialized, +) = MigrationState( + migrationAction = migrationAction, +) diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStore.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStore.kt new file mode 100644 index 0000000000..ed4bff745c --- /dev/null +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStore.kt @@ -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 +} diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt new file mode 100644 index 0000000000..f912759b23 --- /dev/null +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt @@ -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, + ) +} diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/InMemoryMigrationStore.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/InMemoryMigrationStore.kt new file mode 100644 index 0000000000..ba5b63f3cc --- /dev/null +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/InMemoryMigrationStore.kt @@ -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 { + return applicationMigrationVersion + } +} diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt new file mode 100644 index 0000000000..a651362702 --- /dev/null +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt @@ -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, + ) + } +} diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt new file mode 100644 index 0000000000..dd73133060 --- /dev/null +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt @@ -0,0 +1,21 @@ +/* + * 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.rageshake.api.logs + +interface LogFilesRemover { + suspend fun perform() +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt index 5e3b3cfeb1..1fd8526458 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt @@ -26,10 +26,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.features.rageshake.api.crash.CrashDataStore +import io.element.android.features.rageshake.api.logs.LogFilesRemover import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporterListener import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder -import io.element.android.features.rageshake.impl.logs.VectorFileLogger import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope @@ -40,6 +40,7 @@ class BugReportPresenter @Inject constructor( private val bugReporter: BugReporter, private val crashDataStore: CrashDataStore, private val screenshotHolder: ScreenshotHolder, + private val logFilesRemover: LogFilesRemover, private val appCoroutineScope: CoroutineScope, ) : Presenter { private class BugReporterUploadListener( @@ -150,6 +151,6 @@ class BugReportPresenter @Inject constructor( private fun CoroutineScope.resetAll() = launch { screenshotHolder.reset() crashDataStore.reset() - VectorFileLogger.getFromTimber()?.reset() + logFilesRemover.perform() } } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt new file mode 100644 index 0000000000..c0c7c8c346 --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt @@ -0,0 +1,32 @@ +/* + * 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.rageshake.impl.logs + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.rageshake.api.logs.LogFilesRemover +import io.element.android.features.rageshake.impl.reporter.DefaultBugReporter +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultLogFilesRemover @Inject constructor( + private val bugReporter: DefaultBugReporter, +) : LogFilesRemover { + override suspend fun perform() { + bugReporter.deleteAllFiles() + } +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt deleted file mode 100644 index 859f1c961a..0000000000 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (c) 2022 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.rageshake.impl.logs - -import android.content.Context -import android.util.Log -import io.element.android.libraries.androidutils.file.safeDelete -import io.element.android.libraries.core.data.tryOrNull -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import timber.log.Timber -import java.io.File -import java.io.PrintWriter -import java.io.StringWriter -import java.util.logging.FileHandler -import java.util.logging.Level -import java.util.logging.Logger - -/** - * Will be planted in Timber. - */ -class VectorFileLogger( - private val context: Context, - // private val vectorPreferences: VectorPreferences - private val dispatcher: CoroutineDispatcher = Dispatchers.IO, -) : Timber.Tree() { - companion object { - fun getFromTimber(): VectorFileLogger? { - return Timber.forest().filterIsInstance().firstOrNull() - } - - private const val SIZE_20MB = 20 * 1024 * 1024 - // private const val SIZE_50MB = 50 * 1024 * 1024 - } - - /* - private val maxLogSizeByte = if (vectorPreferences.labAllowedExtendedLogging()) SIZE_50MB else SIZE_20MB - private val logRotationCount = if (vectorPreferences.labAllowedExtendedLogging()) 15 else 7 - */ - private val maxLogSizeByte = SIZE_20MB - private val logRotationCount = 7 - - private val logger = Logger.getLogger(context.packageName).apply { - tryOrNull { - useParentHandlers = false - level = Level.ALL - } - } - - private val fileHandler: FileHandler? - private val cacheDirectory get() = File(context.cacheDir, "logs").apply { - if (!exists()) mkdirs() - } - private var fileNamePrefix = "logs" - - private val prioPrefixes = mapOf( - Log.VERBOSE to "V/ ", - Log.DEBUG to "D/ ", - Log.INFO to "I/ ", - Log.WARN to "W/ ", - Log.ERROR to "E/ ", - Log.ASSERT to "WTF/ " - ) - - init { - for (i in 0..15) { - val file = File(cacheDirectory, "elementLogs.$i.txt") - file.safeDelete() - } - - fileHandler = tryOrNull( - onError = { Timber.e(it, "Failed to initialize FileLogger") } - ) { - FileHandler( - cacheDirectory.absolutePath + "/" + fileNamePrefix + ".%g.txt", - maxLogSizeByte, - logRotationCount - ) - .also { it.formatter = LogFormatter() } - .also { logger.addHandler(it) } - } - } - - fun reset() { - // Delete all files - getLogFiles().map { - it.safeDelete() - } - } - - @OptIn(DelicateCoroutinesApi::class) - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - fileHandler ?: return - GlobalScope.launch(dispatcher) { - if (skipLog(priority)) return@launch - if (t != null) { - logToFile(t) - } - logToFile(prioPrefixes[priority] ?: "$priority ", tag ?: "Tag", message) - } - } - - private fun skipLog(priority: Int): Boolean { - // return if (vectorPreferences.labAllowedExtendedLogging()) { - // false - // } else { - // // Exclude verbose logs - // priority < Log.DEBUG - // } - // Exclude verbose logs - return priority < Log.DEBUG - } - - /** - * Adds our own log files to the provided list of files. - * - * @return The list of files with logs. - */ - private fun getLogFiles(): List { - return tryOrNull( - onError = { Timber.e(it, "## getLogFiles() failed") } - ) { - fileHandler - ?.flush() - ?.let { 0 until logRotationCount } - ?.mapNotNull { index -> - File(cacheDirectory, "$fileNamePrefix.$index.txt") - .takeIf { it.exists() } - } - } - .orEmpty() - } - - /** - * Log an Throwable. - * - * @param throwable the throwable to log - */ - private fun logToFile(throwable: Throwable?) { - throwable ?: return - - val errors = StringWriter() - throwable.printStackTrace(PrintWriter(errors)) - - logger.info(errors.toString()) - } - - private fun logToFile(level: String, tag: String, content: String) { - val b = StringBuilder() - b.append(Thread.currentThread().id) - b.append(" ") - b.append(level) - b.append("/") - b.append(tag) - b.append(": ") - b.append(content) - logger.info(b.toString()) - } -} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index aa0633c273..15a0adcf17 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -346,6 +346,12 @@ class DefaultBugReporter @Inject constructor( } } + suspend fun deleteAllFiles() { + withContext(coroutineDispatchers.io) { + getLogFiles().forEach { it.safeDelete() } + } + } + override fun setCurrentTracingFilter(tracingFilter: String) { currentTracingFilter = tracingFilter } @@ -374,7 +380,6 @@ class DefaultBugReporter @Inject constructor( /** * Delete all the log files except the most recent one. - * */ private fun List.deleteAllExceptMostRecent() { if (size > 1) { diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt index 7d6f16d412..e0c033afc7 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt @@ -21,15 +21,18 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.rageshake.api.crash.CrashDataStore +import io.element.android.features.rageshake.api.logs.LogFilesRemover import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder import io.element.android.features.rageshake.test.crash.A_CRASH_DATA import io.element.android.features.rageshake.test.crash.FakeCrashDataStore +import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.A_FAILURE_REASON import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -117,9 +120,11 @@ class BugReportPresenterTest { @Test fun `present - reset all`() = runTest { + val logFilesRemoverLambda = lambdaRecorder { -> } val presenter = createPresenter( crashDataStore = FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), screenshotHolder = FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), + logFilesRemover = FakeLogFilesRemover(logFilesRemoverLambda), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -131,6 +136,7 @@ class BugReportPresenterTest { initialState.eventSink.invoke(BugReportEvents.ResetAll) val resetState = awaitItem() assertThat(resetState.hasCrashLogs).isFalse() + logFilesRemoverLambda.assertions().isCalledExactly(1) // TODO Make it live assertThat(resetState.screenshotUri).isNull() } } @@ -239,10 +245,12 @@ class BugReportPresenterTest { bugReporter: BugReporter = FakeBugReporter(), crashDataStore: CrashDataStore = FakeCrashDataStore(), screenshotHolder: ScreenshotHolder = FakeScreenshotHolder(), + logFilesRemover: LogFilesRemover = FakeLogFilesRemover(lambdaRecorder(ensureNeverCalled = true) { -> }), ) = BugReportPresenter( - bugReporter, - crashDataStore, - screenshotHolder, - this, + bugReporter = bugReporter, + crashDataStore = crashDataStore, + screenshotHolder = screenshotHolder, + logFilesRemover = logFilesRemover, + appCoroutineScope = this, ) } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt deleted file mode 100644 index 26e29e1786..0000000000 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2023 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.rageshake.impl.logs - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment - -@RunWith(RobolectricTestRunner::class) -class VectorFileLoggerTest { - @Test - fun `init VectorFileLogger log debug`() = runTest { - val sut = createVectorFileLogger() - sut.d("A debug log") - } - - @Test - fun `init VectorFileLogger log error`() = runTest { - val sut = createVectorFileLogger() - sut.e(A_THROWABLE, "A debug log") - } - - @Test - fun `reset VectorFileLogger`() = runTest { - val sut = createVectorFileLogger() - sut.reset() - } - - @Test - fun `check getFromTimber`() { - assertThat(VectorFileLogger.getFromTimber()).isNull() - } - - private fun TestScope.createVectorFileLogger() = VectorFileLogger( - context = RuntimeEnvironment.getApplication(), - dispatcher = testCoroutineDispatchers().io, - ) -} diff --git a/features/rageshake/test/build.gradle.kts b/features/rageshake/test/build.gradle.kts index 31d0377f35..c22d2bd205 100644 --- a/features/rageshake/test/build.gradle.kts +++ b/features/rageshake/test/build.gradle.kts @@ -24,4 +24,5 @@ android { dependencies { implementation(projects.features.rageshake.api) implementation(libs.coroutines.core) + implementation(projects.tests.testutils) } diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt new file mode 100644 index 0000000000..a3c927fe41 --- /dev/null +++ b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt @@ -0,0 +1,28 @@ +/* + * 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.rageshake.test.logs + +import io.element.android.features.rageshake.api.logs.LogFilesRemover +import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder + +class FakeLogFilesRemover( + private val performLambda: LambdaNoParamRecorder, +) : LogFilesRemover { + override suspend fun perform() { + performLambda() + } +} diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 05d6aa1b24..8366daf9c4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -164,6 +164,7 @@ "People" "Permalink" "Permission" + "Please wait…" "Are you sure you want to end this poll?" "Poll: %1$s" "Total votes: %1$s"