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
@@ -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)
|
||||
|
||||
@@ -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<MainNode> {
|
||||
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<MainNode> {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<BugReportState> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<VectorFileLogger>().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<File> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<File>.deleteAllExceptMostRecent() {
|
||||
if (size > 1) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -24,4 +24,5 @@ android {
|
||||
dependencies {
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
||||
@@ -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<Unit>,
|
||||
) : LogFilesRemover {
|
||||
override suspend fun perform() {
|
||||
performLambda()
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,7 @@
|
||||
<string name="common_people">"People"</string>
|
||||
<string name="common_permalink">"Permalink"</string>
|
||||
<string name="common_permission">"Permission"</string>
|
||||
<string name="common_please_wait">"Please wait…"</string>
|
||||
<string name="common_poll_end_confirmation">"Are you sure you want to end this poll?"</string>
|
||||
<string name="common_poll_summary">"Poll: %1$s"</string>
|
||||
<string name="common_poll_total_votes">"Total votes: %1$s"</string>
|
||||
|
||||
Reference in New Issue
Block a user