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