Add clear cache action in the developer settings (#643)

This commit is contained in:
Benoit Marty 2023-06-20 12:52:20 +02:00 committed by Benoit Marty
parent 9829daa70e
commit f014f0a3ae
13 changed files with 172 additions and 6 deletions

View file

@ -62,6 +62,7 @@ dependencies {
testImplementation(projects.features.logout.impl)
testImplementation(projects.features.analytics.test)
testImplementation(projects.features.analytics.impl)
testImplementation(projects.tests.testutils)
androidTestImplementation(libs.test.junitext)
}

View file

@ -20,4 +20,5 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
sealed interface DeveloperSettingsEvents {
data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents
object ClearCache: DeveloperSettingsEvents
}

View file

@ -18,12 +18,17 @@ package io.element.android.features.preferences.impl.developer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateMap
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -36,6 +41,7 @@ import javax.inject.Inject
class DeveloperSettingsPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService,
private val clearCacheUseCase: ClearCacheUseCase,
) : Presenter<DeveloperSettingsState> {
@Composable
@ -47,6 +53,9 @@ class DeveloperSettingsPresenter @Inject constructor(
val enabledFeatures = remember {
mutableStateMapOf<String, Boolean>()
}
val clearCacheAction = remember {
mutableStateOf<Async<Unit>>(Async.Uninitialized)
}
LaunchedEffect(Unit) {
FeatureFlags.values().forEach { feature ->
features[feature.key] = feature
@ -64,11 +73,13 @@ class DeveloperSettingsPresenter @Inject constructor(
event.feature,
event.isEnabled
)
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
}
}
return DeveloperSettingsState(
features = featureUiModels.toImmutableList(),
clearCacheAction = clearCacheAction.value,
eventSink = ::handleEvents
)
}
@ -103,6 +114,12 @@ class DeveloperSettingsPresenter @Inject constructor(
enabledFeatures[featureUiModel.key] = enabled
}
}
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch {
suspend {
clearCacheUseCase.execute()
}.execute(clearCacheAction)
}
}

View file

@ -16,10 +16,12 @@
package io.element.android.features.preferences.impl.developer
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList
data class DeveloperSettingsState(
data class DeveloperSettingsState constructor(
val features: ImmutableList<FeatureUiModel>,
val clearCacheAction: Async<Unit>,
val eventSink: (DeveloperSettingsEvents) -> Unit
)

View file

@ -17,16 +17,19 @@
package io.element.android.features.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> {
override val values: Sequence<DeveloperSettingsState>
get() = sequenceOf(
aDeveloperSettingsState(),
aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()),
)
}
fun aDeveloperSettingsState() = DeveloperSettingsState(
features = aFeatureUiModelList(),
clearCacheAction = Async.Uninitialized,
eventSink = {}
)

View file

@ -16,11 +16,14 @@
package io.element.android.features.preferences.impl.developer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
@ -52,6 +55,18 @@ fun DeveloperSettingsView(
onClick = onOpenShowkase
)
}
PreferenceCategory(title = "Cache") {
PreferenceText(
title = "Clear cache",
icon = Icons.Default.Delete,
loadingCurrentValue = state.clearCacheAction.isLoading(),
onClick = {
if (state.clearCacheAction.isLoading().not()) {
state.eventSink(DeveloperSettingsEvents.ClearCache)
}
}
)
}
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.preferences.impl.tasks
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.withContext
import javax.inject.Inject
interface ClearCacheUseCase {
suspend fun execute()
}
@ContributesBinding(SessionScope::class)
class DefaultClearCacheUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers,
) : ClearCacheUseCase {
override suspend fun execute() = withContext(coroutineDispatchers.io) {
matrixClient.stopSync()
matrixClient.clearCache()
context.cacheDir.deleteRecursively()
matrixClient.startSync()
}
}

View file

@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import kotlinx.coroutines.test.runTest
@ -29,13 +31,15 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures initial state is correct`() = runTest {
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService()
FakeFeatureFlagService(),
FakeClearCacheUseCase()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.features).isEmpty()
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized)
cancelAndIgnoreRemainingEvents()
}
}
@ -43,7 +47,8 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures feature list is loaded`() = runTest {
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService()
FakeFeatureFlagService(),
FakeClearCacheUseCase()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -58,7 +63,8 @@ class DeveloperSettingsPresenterTest {
@Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService()
FakeFeatureFlagService(),
FakeClearCacheUseCase()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -74,4 +80,25 @@ class DeveloperSettingsPresenterTest {
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - clear cache`() = runTest {
val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = DeveloperSettingsPresenter(
FakeFeatureFlagService(),
clearCacheUseCase
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
initialState.eventSink(DeveloperSettingsEvents.ClearCache)
val stateAfterEvent = awaitItem()
assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().clearCacheAction).isInstanceOf(Async.Success::class.java)
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
}
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.preferences.impl.tasks
import io.element.android.tests.testutils.simulateLongTask
class FakeClearCacheUseCase : ClearCacheUseCase {
var executeHasBeenCalled = false
private set
override suspend fun execute() = simulateLongTask {
executeHasBeenCalled = true
}
}

View file

@ -50,6 +50,7 @@ interface MatrixClient : Closeable {
fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService
fun notificationService(): NotificationService
suspend fun clearCache()
suspend fun logout()
suspend fun loadUserDisplayName(): Result<String>
suspend fun loadUserAvatarURLString(): Result<String?>

View file

@ -32,6 +32,7 @@ dependencies {
// api(projects.libraries.rustsdk)
implementation(libs.matrix.sdk)
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.services.toolbox.api)
api(projects.libraries.matrix.api)
implementation(libs.dagger)

View file

@ -18,6 +18,7 @@
package io.element.android.libraries.matrix.impl
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback
@ -336,6 +337,10 @@ class RustMatrixClient constructor(
client.destroy()
}
override suspend fun clearCache() {
baseDirectory.deleteSessionDirectory(userID = client.userId(), deleteCryptoDb = false)
}
override suspend fun logout() = withContext(dispatchers.io) {
try {
client.logout()
@ -378,11 +383,29 @@ class RustMatrixClient constructor(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
private fun File.deleteSessionDirectory(userID: String): Boolean {
private fun File.deleteSessionDirectory(
userID: String,
deleteCryptoDb: Boolean = false,
): Boolean {
// Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_")
val sessionDirectory = File(this, sanitisedUserID)
return sessionDirectory.deleteRecursively()
return if (deleteCryptoDb) {
// Delete the folder and all its content
sessionDirectory.deleteRecursively()
} else {
// Delete only the state.db file
listOf(
"matrix-sdk-state.sqlite3",
"matrix-sdk-state.sqlite3-shm",
"matrix-sdk-state.sqlite3-wal",
).map { fileName ->
File(sessionDirectory, fileName)
}.forEach { file ->
file.safeDelete()
}
true
}
}
}

View file

@ -102,6 +102,9 @@ class FakeMatrixClient(
override fun stopSync() = Unit
override suspend fun clearCache() {
}
override suspend fun logout() {
delay(100)
logoutFailure?.let { throw it }