Browse Source

Implement a migration mechanism to handle internal stuff which need to occur during application upgrade.

Remove VectorFileLogger, it was dead code.
pull/2749/head
Benoit Marty 5 months ago committed by Benoit Marty
parent
commit
7bbcb719d5
  1. 1
      app/build.gradle.kts
  2. 39
      app/src/main/kotlin/io/element/android/x/MainActivity.kt
  3. 3
      app/src/main/kotlin/io/element/android/x/di/AppBindings.kt
  4. 27
      features/migration/api/build.gradle.kts
  5. 31
      features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationEntryPoint.kt
  6. 23
      features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationState.kt
  7. 44
      features/migration/impl/build.gradle.kts
  8. 42
      features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt
  9. 52
      features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt
  10. 73
      features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt
  11. 35
      features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt
  12. 24
      features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStore.kt
  13. 65
      features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt
  14. 34
      features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/InMemoryMigrationStore.kt
  15. 87
      features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt
  16. 21
      features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt
  17. 5
      features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt
  18. 32
      features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt
  19. 176
      features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt
  20. 7
      features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
  21. 16
      features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt
  22. 58
      features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt
  23. 1
      features/rageshake/test/build.gradle.kts
  24. 28
      features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt
  25. 1
      libraries/ui-strings/src/main/res/values/localazy.xml

1
app/build.gradle.kts

@ -219,6 +219,7 @@ dependencies { @@ -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)

39
app/src/main/kotlin/io/element/android/x/MainActivity.kt

@ -86,6 +86,7 @@ class MainActivity : NodeActivity() { @@ -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() { @@ -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() { @@ -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);

3
app/src/main/kotlin/io/element/android/x/di/AppBindings.kt

@ -17,6 +17,7 @@ @@ -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 { @@ -35,4 +36,6 @@ interface AppBindings {
fun lockScreenService(): LockScreenService
fun preferencesStore(): AppPreferencesStore
fun migrationEntryPoint(): MigrationEntryPoint
}

27
features/migration/api/build.gradle.kts

@ -0,0 +1,27 @@ @@ -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)
}

31
features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationEntryPoint.kt

@ -0,0 +1,31 @@ @@ -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,
)
}

23
features/migration/api/src/main/kotlin/io/element/android/features/api/MigrationState.kt

@ -0,0 +1,23 @@ @@ -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

@ -0,0 +1,44 @@ @@ -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)
}

42
features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationEntryPoint.kt

@ -0,0 +1,42 @@ @@ -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,
)
}

52
features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt

@ -0,0 +1,52 @@ @@ -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
}
}
}

73
features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt

@ -0,0 +1,73 @@ @@ -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
}
}

35
features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt

@ -0,0 +1,35 @@ @@ -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,
)

24
features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStore.kt

@ -0,0 +1,24 @@ @@ -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>
}

65
features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt

@ -0,0 +1,65 @@ @@ -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,
)
}

34
features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/InMemoryMigrationStore.kt

@ -0,0 +1,34 @@ @@ -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
}
}

87
features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt

@ -0,0 +1,87 @@ @@ -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,
)
}
}

21
features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt

@ -0,0 +1,21 @@ @@ -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()
}

5
features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt

@ -26,10 +26,10 @@ import androidx.compose.runtime.mutableStateOf @@ -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( @@ -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( @@ -150,6 +151,6 @@ class BugReportPresenter @Inject constructor(
private fun CoroutineScope.resetAll() = launch {
screenshotHolder.reset()
crashDataStore.reset()
VectorFileLogger.getFromTimber()?.reset()
logFilesRemover.perform()
}
}

32
features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt

@ -0,0 +1,32 @@ @@ -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()
}
}

176
features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt

@ -1,176 +0,0 @@ @@ -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())
}
}

7
features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt

@ -346,6 +346,12 @@ class DefaultBugReporter @Inject constructor( @@ -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( @@ -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) {

16
features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt

@ -21,15 +21,18 @@ import app.cash.molecule.moleculeFlow @@ -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 { @@ -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 { @@ -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 { @@ -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,
)
}

58
features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt

@ -1,58 +0,0 @@ @@ -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,
)
}

1
features/rageshake/test/build.gradle.kts

@ -24,4 +24,5 @@ android { @@ -24,4 +24,5 @@ android {
dependencies {
implementation(projects.features.rageshake.api)
implementation(libs.coroutines.core)
implementation(projects.tests.testutils)
}

28
features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt

@ -0,0 +1,28 @@ @@ -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()
}
}

1
libraries/ui-strings/src/main/res/values/localazy.xml

@ -164,6 +164,7 @@ @@ -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>

Loading…
Cancel
Save