Benoit Marty
5 months ago
committed by
Benoit Marty
25 changed files with 671 additions and 254 deletions
@ -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, |
||||||
|
) |
@ -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() |
||||||
|
} |
@ -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()) |
|
||||||
} |
|
||||||
} |
|
@ -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, |
|
||||||
) |
|
||||||
} |
|
@ -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() |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue