diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml deleted file mode 100644 index dd650c15e1..0000000000 --- a/.idea/dictionaries/bmarty.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - homeserver - - - \ No newline at end of file diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml index 7c04ccd5e7..9353e11fd9 100644 --- a/.idea/dictionaries/shared.xml +++ b/.idea/dictionaries/shared.xml @@ -2,8 +2,10 @@ backstack + homeserver kover onboarding + showkase textfields diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 78c39f93e6..733485fcef 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -44,6 +44,7 @@ import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow +import io.element.android.features.preferences.api.CacheService import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -54,7 +55,9 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor( @Assisted val buildContext: BuildContext, @Assisted plugins: List, private val authenticationService: MatrixAuthenticationService, + private val cacheService: CacheService, private val matrixClientsHolder: MatrixClientsHolder, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, @@ -88,11 +92,19 @@ class RootFlowNode @AssistedInject constructor( private fun observeLoggedInState() { authenticationService.isLoggedIn() .distinctUntilChanged() - .onEach { isLoggedIn -> - Timber.v("isLoggedIn=$isLoggedIn") + .combine( + cacheService.cacheIndex().onEach { + Timber.v("cacheIndex=$it") + matrixClientsHolder.removeAll() + } + ) { isLoggedIn, cacheIdx -> isLoggedIn to cacheIdx } + .onEach { pair -> + val isLoggedIn = pair.first + val cacheIndex = pair.second + Timber.v("isLoggedIn=$isLoggedIn, cacheIndex=$cacheIndex") if (isLoggedIn) { tryToRestoreLatestSession( - onSuccess = { switchToLoggedInFlow(it) }, + onSuccess = { switchToLoggedInFlow(it, cacheIndex) }, onFailure = { switchToNotLoggedInFlow() } ) } else { @@ -102,8 +114,8 @@ class RootFlowNode @AssistedInject constructor( .launchIn(lifecycleScope) } - private fun switchToLoggedInFlow(sessionId: SessionId) { - backstack.safeRoot(NavTarget.LoggedInFlow(sessionId)) + private fun switchToLoggedInFlow(sessionId: SessionId, cacheIndex: Int) { + backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex)) } private fun switchToNotLoggedInFlow() { @@ -163,7 +175,7 @@ class RootFlowNode @AssistedInject constructor( object NotLoggedInFlow : NavTarget @Parcelize - data class LoggedInFlow(val sessionId: SessionId) : NavTarget + data class LoggedInFlow(val sessionId: SessionId, val cacheIndex: Int) : NavTarget @Parcelize object BugReport : NavTarget @@ -235,8 +247,9 @@ class RootFlowNode @AssistedInject constructor( } private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { + val cacheIndex = cacheService.cacheIndex().first() return attachChild { - backstack.newRoot(NavTarget.LoggedInFlow(sessionId)) + backstack.newRoot(NavTarget.LoggedInFlow(sessionId, cacheIndex)) } } } diff --git a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt b/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt index 3d969567f7..7ff0f50d9c 100644 --- a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt +++ b/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt @@ -29,9 +29,9 @@ class FakeAnalyticsService( didAskUserConsent: Boolean = false ): AnalyticsService { - private var isEnabledFlow = MutableStateFlow(isEnabled) - private var didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) - var capturedEvents = mutableListOf() + private val isEnabledFlow = MutableStateFlow(isEnabled) + private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) + val capturedEvents = mutableListOf() override fun getAvailableAnalyticsProviders(): List = emptyList() diff --git a/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt b/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt index 3716cd4456..486d3fb4a8 100644 --- a/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt +++ b/features/invitelist/test/src/main/kotlin/io/element/android/features/invitelist/test/FakeSeenInvitesStore.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeSeenInvitesStore : SeenInvitesStore { - private var existing = MutableStateFlow(emptySet()) + private val existing = MutableStateFlow(emptySet()) private var provided: Set? = null fun publishRoomIds(invites: Set) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt index 7500f0d91e..ff2f8aaeeb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -21,7 +21,7 @@ import android.net.Uri import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor -import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter +import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileSize import io.element.android.libraries.androidutils.file.getMimeType diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index c99ab46ab1..d9a12cf615 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -25,8 +25,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor -import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter import io.element.android.features.messages.impl.timeline.util.toHtmlDocument +import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt index beb37f8e51..41daff47fd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt @@ -32,7 +32,7 @@ import io.element.android.features.messages.impl.timeline.factories.virtual.Time import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation -import io.element.android.features.messages.timeline.FakeFileSizeFormatter +import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt new file mode 100644 index 0000000000..0bc9285853 --- /dev/null +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt @@ -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.api + +import kotlinx.coroutines.flow.Flow + +interface CacheService { + /** + * Returns a flow of the current cache index, can let the app to know when the + * cache has been cleared, for instance to restart the app. + * Will be a flow of Int, starting from 0, and incrementing each time the cache is cleared. + */ + fun cacheIndex(): Flow +} diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 631884fe47..1e76ee5c93 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -32,6 +32,7 @@ anvil { dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -39,6 +40,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.ui) implementation(projects.libraries.elementresources) + implementation(projects.libraries.network) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.features.rageshake.api) @@ -47,6 +49,7 @@ dependencies { implementation(projects.features.logout.api) implementation(libs.datetime) implementation(libs.accompanist.placeholder) + implementation(libs.coil.compose) api(projects.features.preferences.api) ksp(libs.showkase.processor) @@ -62,6 +65,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) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileSizeFormatter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt similarity index 56% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileSizeFormatter.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt index 4ede9b7f21..7675ec3dd6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileSizeFormatter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultCacheService.kt @@ -14,25 +14,26 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.timeline.util +package io.element.android.features.preferences.impl -import android.content.Context -import android.text.format.Formatter import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.preferences.api.CacheService import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject -interface FileSizeFormatter { - /** - * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc. - */ - fun format(fileSize: Long): String -} - +@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class AndroidFileSizeFormatter @Inject constructor(@ApplicationContext private val context: Context) : FileSizeFormatter { - override fun format(fileSize: Long): String { - return Formatter.formatShortFileSize(context, fileSize) +class DefaultCacheService @Inject constructor() : CacheService { + private val cacheIndexState = MutableStateFlow(0) + + override fun cacheIndex(): Flow { + return cacheIndexState + } + + fun incrementCacheIndex() { + cacheIndexState.value++ } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index b79484592f..bb3879b129 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -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 } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 9f9cd636eb..d4430dd2e3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -18,12 +18,18 @@ 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.features.preferences.impl.tasks.ComputeCacheSizeUseCase +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 +42,8 @@ import javax.inject.Inject class DeveloperSettingsPresenter @Inject constructor( private val featureFlagService: FeatureFlagService, + private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, + private val clearCacheUseCase: ClearCacheUseCase, ) : Presenter { @Composable @@ -47,6 +55,12 @@ class DeveloperSettingsPresenter @Inject constructor( val enabledFeatures = remember { mutableStateMapOf() } + val cacheSize = remember { + mutableStateOf>(Async.Uninitialized) + } + val clearCacheAction = remember { + mutableStateOf>(Async.Uninitialized) + } LaunchedEffect(Unit) { FeatureFlags.values().forEach { feature -> features[feature.key] = feature @@ -55,6 +69,10 @@ class DeveloperSettingsPresenter @Inject constructor( } val featureUiModels = createUiModels(features, enabledFeatures) val coroutineScope = rememberCoroutineScope() + // Compute cache size each time the clear cache action value is changed + LaunchedEffect(clearCacheAction.value) { + computeCacheSize(cacheSize) + } fun handleEvents(event: DeveloperSettingsEvents) { when (event) { @@ -64,11 +82,14 @@ class DeveloperSettingsPresenter @Inject constructor( event.feature, event.isEnabled ) + DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) } } return DeveloperSettingsState( features = featureUiModels.toImmutableList(), + cacheSize = cacheSize.value, + clearCacheAction = clearCacheAction.value, eventSink = ::handleEvents ) } @@ -103,6 +124,18 @@ class DeveloperSettingsPresenter @Inject constructor( enabledFeatures[featureUiModel.key] = enabled } } + + private fun CoroutineScope.computeCacheSize(cacheSize: MutableState>) = launch { + suspend { + computeCacheSizeUseCase() + }.execute(cacheSize) + } + + private fun CoroutineScope.clearCache(clearCacheAction: MutableState>) = launch { + suspend { + clearCacheUseCase() + }.execute(clearCacheAction) + } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index 53ff80967e..61205e7f7d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -16,10 +16,13 @@ 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, + val cacheSize: Async, + val clearCacheAction: Async, val eventSink: (DeveloperSettingsEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index f69f73e6e5..de94bd6664 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -17,16 +17,20 @@ 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 { override val values: Sequence get() = sequenceOf( aDeveloperSettingsState(), + aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()), ) } fun aDeveloperSettingsState() = DeveloperSettingsState( features = aFeatureUiModelList(), + cacheSize = Async.Success("1.2 MB"), + clearCacheAction = Async.Uninitialized, eventSink = {} ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 697081c397..027b3cfd1d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -16,11 +16,15 @@ 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.Async +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 +56,20 @@ fun DeveloperSettingsView( onClick = onOpenShowkase ) } + val cache = state.cacheSize + PreferenceCategory(title = "Cache") { + PreferenceText( + title = "Clear cache", + icon = Icons.Default.Delete, + currentValue = cache.dataOrNull(), + loadingCurrentValue = state.cacheSize.isLoading() || state.clearCacheAction.isLoading(), + onClick = { + if (state.clearCacheAction.isLoading().not()) { + state.eventSink(DeveloperSettingsEvents.ClearCache) + } + } + ) + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt new file mode 100644 index 0000000000..f7b0d01130 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt @@ -0,0 +1,62 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoilApi::class) + +package io.element.android.features.preferences.impl.tasks + +import android.content.Context +import coil.Coil +import coil.annotation.ExperimentalCoilApi +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.preferences.impl.DefaultCacheService +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 okhttp3.OkHttpClient +import javax.inject.Inject +import javax.inject.Provider + +interface ClearCacheUseCase { + suspend operator fun invoke() +} + +@ContributesBinding(SessionScope::class) +class DefaultClearCacheUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val matrixClient: MatrixClient, + private val coroutineDispatchers: CoroutineDispatchers, + private val defaultCacheIndexProvider: DefaultCacheService, + private val okHttpClient: Provider, +) : ClearCacheUseCase { + override suspend fun invoke() = withContext(coroutineDispatchers.io) { + // Clear Matrix cache + matrixClient.clearCache() + // Clear Coil cache + Coil.imageLoader(context).let { + it.diskCache?.clear() + it.memoryCache?.clear() + } + // Clear OkHttp cache + okHttpClient.get().cache?.delete() + // Clear app cache + context.cacheDir.deleteRecursively() + // Ensure the app is restarted + defaultCacheIndexProvider.incrementCacheIndex() + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt new file mode 100644 index 0000000000..661f6493ec --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ComputeCacheSizeUseCase.kt @@ -0,0 +1,48 @@ +/* + * 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.androidutils.filesize.FileSizeFormatter +import io.element.android.libraries.androidutils.file.getSizeOfFiles +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 ComputeCacheSizeUseCase { + suspend operator fun invoke(): String +} + +@ContributesBinding(SessionScope::class) +class DefaultComputeCacheSizeUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val matrixClient: MatrixClient, + private val coroutineDispatchers: CoroutineDispatchers, + private val fileSizeFormatter: FileSizeFormatter, +) : ComputeCacheSizeUseCase { + override suspend fun invoke(): String = withContext(coroutineDispatchers.io) { + var cumulativeSize = 0L + cumulativeSize += matrixClient.getCacheSize() + // - 4096 to not include the size fo the folder + cumulativeSize += (context.cacheDir.getSizeOfFiles() - 4096).coerceAtLeast(0) + fileSizeFormatter.format(cumulativeSize) + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 6b7c8c2df4..226140647d 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -20,6 +20,9 @@ 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.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase +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 +32,17 @@ class DeveloperSettingsPresenterTest { @Test fun `present - ensures initial state is correct`() = runTest { val presenter = DeveloperSettingsPresenter( - FakeFeatureFlagService() + FakeFeatureFlagService(), + FakeComputeCacheSizeUseCase(), + FakeClearCacheUseCase(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.features).isEmpty() + assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized) + assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized) cancelAndIgnoreRemainingEvents() } } @@ -43,7 +50,9 @@ class DeveloperSettingsPresenterTest { @Test fun `present - ensures feature list is loaded`() = runTest { val presenter = DeveloperSettingsPresenter( - FakeFeatureFlagService() + FakeFeatureFlagService(), + FakeComputeCacheSizeUseCase(), + FakeClearCacheUseCase(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -58,7 +67,9 @@ class DeveloperSettingsPresenterTest { @Test fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { val presenter = DeveloperSettingsPresenter( - FakeFeatureFlagService() + FakeFeatureFlagService(), + FakeComputeCacheSizeUseCase(), + FakeClearCacheUseCase(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -74,4 +85,28 @@ class DeveloperSettingsPresenterTest { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `present - clear cache`() = runTest { + val clearCacheUseCase = FakeClearCacheUseCase() + val presenter = DeveloperSettingsPresenter( + FakeFeatureFlagService(), + FakeComputeCacheSizeUseCase(), + 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) + skipItems(1) + assertThat(awaitItem().clearCacheAction).isInstanceOf(Async.Success::class.java) + assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue() + cancelAndIgnoreRemainingEvents() + } + } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeClearCacheUseCase.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeClearCacheUseCase.kt new file mode 100644 index 0000000000..7415e09e96 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeClearCacheUseCase.kt @@ -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 invoke() = simulateLongTask { + executeHasBeenCalled = true + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeComputeCacheSizeUseCase.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeComputeCacheSizeUseCase.kt new file mode 100644 index 0000000000..fa8556630f --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/FakeComputeCacheSizeUseCase.kt @@ -0,0 +1,25 @@ +/* + * 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 FakeComputeCacheSizeUseCase : ComputeCacheSizeUseCase { + override suspend fun invoke() = simulateLongTask { + "O kB" + } +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 874da06acb..91f761bda9 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -52,6 +52,7 @@ import java.io.OutputStreamWriter import java.net.HttpURLConnection import java.util.Locale import javax.inject.Inject +import javax.inject.Provider /** * BugReporter creates and sends the bug reports. @@ -62,7 +63,7 @@ class DefaultBugReporter @Inject constructor( private val screenshotHolder: ScreenshotHolder, private val crashDataStore: CrashDataStore, private val coroutineDispatchers: CoroutineDispatchers, - private val okHttpClient: OkHttpClient, + private val okHttpClient: Provider, /* private val activeSessionHolder: ActiveSessionHolder, private val versionProvider: VersionProvider, @@ -339,7 +340,7 @@ class DefaultBugReporter @Inject constructor( // trigger the request try { - mBugReportCall = okHttpClient.newCall(request) + mBugReportCall = okHttpClient.get().newCall(request) response = mBugReportCall!!.execute() responseCode = response.code } catch (e: Exception) { diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt index 269407d3b5..ea214ff683 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt @@ -17,9 +17,11 @@ package io.element.android.libraries.androidutils.file import android.content.Context +import androidx.annotation.WorkerThread import io.element.android.libraries.core.data.tryOrNull import timber.log.Timber import java.io.File +import java.util.Locale import java.util.UUID fun File.safeDelete() { @@ -52,3 +54,99 @@ fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): val suffix = extension?.let { ".$extension" } return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() } } + +// Implementation should return true in case of success +typealias ActionOnFile = (file: File) -> Boolean + +/* ========================================================================================== + * Log + * ========================================================================================== */ + +fun lsFiles(context: Context) { + Timber.v("Content of cache dir:") + recursiveActionOnFile(context.cacheDir, ::logAction) + + Timber.v("Content of files dir:") + recursiveActionOnFile(context.filesDir, ::logAction) +} + +private fun logAction(file: File): Boolean { + if (file.isDirectory) { + Timber.v(file.toString()) + } else { + Timber.v("$file ${file.length()} bytes") + } + return true +} + +/* ========================================================================================== + * Private + * ========================================================================================== */ + +/** + * Return true in case of success. + */ +private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean { + if (file.isDirectory) { + file.list()?.forEach { + val result = recursiveActionOnFile(File(file, it), action) + + if (!result) { + // Break the loop + return false + } + } + } + + return action.invoke(file) +} + +/** + * Get the file extension of a fileUri or a filename. + * + * @param fileUri the fileUri (can be a simple filename) + * @return the file extension, in lower case, or null is extension is not available or empty + */ +fun getFileExtension(fileUri: String): String? { + var reducedStr = fileUri + + if (reducedStr.isNotEmpty()) { + // Remove fragment + reducedStr = reducedStr.substringBeforeLast('#') + + // Remove query + reducedStr = reducedStr.substringBeforeLast('?') + + // Remove path + val filename = reducedStr.substringAfterLast('/') + + // Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern + // See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs + if (filename.isNotEmpty()) { + val dotPos = filename.lastIndexOf('.') + if (0 <= dotPos) { + val ext = filename.substring(dotPos + 1) + + if (ext.isNotBlank()) { + return ext.lowercase(Locale.ROOT) + } + } + } + } + + return null +} + +/* ========================================================================================== + * Size + * ========================================================================================== */ + +@WorkerThread +fun File.getSizeOfFiles(): Long { + return walkTopDown() + .onEnter { + Timber.v("Get size of ${it.absolutePath}") + true + } + .sumOf { it.length() } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt new file mode 100644 index 0000000000..9cd70febcc --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt @@ -0,0 +1,52 @@ +/* + * 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.libraries.androidutils.filesize + +import android.content.Context +import android.os.Build +import android.text.format.Formatter +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class AndroidFileSizeFormatter @Inject constructor( + @ApplicationContext private val context: Context, + ) : FileSizeFormatter { + override fun format(fileSize: Long, useShortFormat: Boolean): String { + // Since Android O, the system considers that 1kB = 1000 bytes instead of 1024 bytes. + // We want to avoid that. + val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + fileSize + } else { + // First convert the size + when { + fileSize < 1024 -> fileSize + fileSize < 1024 * 1024 -> fileSize * 1000 / 1024 + fileSize < 1024 * 1024 * 1024 -> fileSize * 1000 / 1024 * 1000 / 1024 + else -> fileSize * 1000 / 1024 * 1000 / 1024 * 1000 / 1024 + } + } + + return if (useShortFormat) { + Formatter.formatShortFileSize(context, normalizedSize) + } else { + Formatter.formatFileSize(context, normalizedSize) + } + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/FakeFileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FakeFileSizeFormatter.kt similarity index 78% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/FakeFileSizeFormatter.kt rename to libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FakeFileSizeFormatter.kt index 4ff65b0146..32c0239428 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/FakeFileSizeFormatter.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FakeFileSizeFormatter.kt @@ -14,12 +14,10 @@ * limitations under the License. */ -package io.element.android.features.messages.timeline - -import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter +package io.element.android.libraries.androidutils.filesize class FakeFileSizeFormatter : FileSizeFormatter { - override fun format(fileSize: Long): String { + override fun format(fileSize: Long, useShortFormat: Boolean): String { return "$fileSize Bytes" } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FileSizeFormatter.kt new file mode 100644 index 0000000000..7be38bf9bd --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/FileSizeFormatter.kt @@ -0,0 +1,24 @@ +/* + * 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.libraries.androidutils.filesize + +interface FileSizeFormatter { + /** + * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc. + */ + fun format(fileSize: Long, useShortFormat: Boolean = true): String +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt index 2172f4518a..2195be296f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.designsystem.components.preferences import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -26,7 +27,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.progressSemantics import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport @@ -55,7 +55,7 @@ fun PreferenceText( tintColor: Color? = null, onClick: () -> Unit = {}, ) { - val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight + val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight Box( modifier = modifier .fillMaxWidth() @@ -69,9 +69,10 @@ fun PreferenceText( .padding(vertical = preferencePaddingVertical) ) { PreferenceIcon(icon = icon, tintColor = tintColor) - Column(modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) + Column( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) ) { if (title != null) { Text( @@ -92,15 +93,24 @@ fun PreferenceText( } } if (currentValue != null) { - Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary) - Spacer(Modifier.width(16.dp)) + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(horizontal = 16.dp), + text = currentValue, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary, + ) } else if (loadingCurrentValue) { - CircularProgressIndicator(modifier = Modifier - .progressSemantics() - .size(20.dp), strokeWidth = 2.dp) - Spacer(Modifier.width(16.dp)) + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .padding(horizontal = 16.dp) + .size(20.dp) + .align(Alignment.CenterVertically), + strokeWidth = 2.dp + ) } - } } } @@ -111,9 +121,39 @@ internal fun PreferenceTextPreview() = ElementThemedPreview { ContentToPreview() @Composable private fun ContentToPreview() { - PreferenceText( - title = "Title", - subtitle = "Some content", - icon = Icons.Default.BugReport, - ) + Column( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + PreferenceText( + title = "Title", + icon = Icons.Default.BugReport, + ) + PreferenceText( + title = "Title", + subtitle = "Some content", + icon = Icons.Default.BugReport, + ) + PreferenceText( + title = "Title", + subtitle = "Some content", + icon = Icons.Default.BugReport, + currentValue = "123", + ) + PreferenceText( + title = "Title", + subtitle = "Some content", + icon = Icons.Default.BugReport, + loadingCurrentValue = true, + ) + PreferenceText( + title = "Title", + icon = Icons.Default.BugReport, + currentValue = "123", + ) + PreferenceText( + title = "Title", + icon = Icons.Default.BugReport, + loadingCurrentValue = true, + ) + } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 4a018e18da..0bd7a8ab22 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -50,6 +50,12 @@ interface MatrixClient : Closeable { fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService fun notificationService(): NotificationService + suspend fun getCacheSize(): Long + + /** + * Will close the client and delete the cache data. + */ + suspend fun clearCache() suspend fun logout() suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 1ac5b4b507..5709b5a6d7 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -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) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 268e09c764..be31855ecd 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -18,6 +18,8 @@ package io.element.android.libraries.matrix.impl +import io.element.android.libraries.androidutils.file.getSizeOfFiles +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,15 +338,25 @@ class RustMatrixClient constructor( client.destroy() } + override suspend fun getCacheSize(): Long { + // Do not use client.userId since it can throw if client has been closed (during clear cache) + return baseDirectory.getCacheSize(userID = sessionId.value) + } + + override suspend fun clearCache() { + close() + baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false) + } + override suspend fun logout() = withContext(dispatchers.io) { try { client.logout() } catch (failure: Throwable) { Timber.e(failure, "Fail to call logout on HS. Still delete local files.") } - baseDirectory.deleteSessionDirectory(userID = client.userId()) - sessionStore.removeSession(client.userId()) close() + baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true) + sessionStore.removeSession(sessionId.value) } override suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) { @@ -378,11 +390,48 @@ class RustMatrixClient constructor( override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver - private fun File.deleteSessionDirectory(userID: String): Boolean { + private suspend fun File.getCacheSize( + userID: String, + includeCryptoDb: Boolean = false, + ): Long = withContext(dispatchers.io) { // Rust sanitises the user ID replacing invalid characters with an _ val sanitisedUserID = userID.replace(":", "_") - val sessionDirectory = File(this, sanitisedUserID) - return sessionDirectory.deleteRecursively() + val sessionDirectory = File(this@getCacheSize, sanitisedUserID) + if (includeCryptoDb) { + sessionDirectory.getSizeOfFiles() + } else { + listOf( + "matrix-sdk-state.sqlite3", + "matrix-sdk-state.sqlite3-shm", + "matrix-sdk-state.sqlite3-wal", + ).map { fileName -> + File(sessionDirectory, fileName) + }.sumOf { file -> + file.length() + } + } + } + + private suspend fun File.deleteSessionDirectory( + userID: String, + deleteCryptoDb: Boolean = false, + ): Boolean = withContext(dispatchers.io) { + // Rust sanitises the user ID replacing invalid characters with an _ + val sanitisedUserID = userID.replace(":", "_") + val sessionDirectory = File(this@deleteSessionDirectory, sanitisedUserID) + if (deleteCryptoDb) { + // Delete the folder and all its content + sessionDirectory.deleteRecursively() + } else { + // Delete only the state.db file + sessionDirectory.listFiles().orEmpty() + .filter { it.name.contains("matrix-sdk-state") } + .forEach { file -> + Timber.w("Deleting file ${file.name}...") + file.safeDelete() + } + true + } } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 8b38a74457..eb5e4624d6 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -102,6 +102,13 @@ class FakeMatrixClient( override fun stopSync() = Unit + override suspend fun getCacheSize(): Long { + return 0 + } + + override suspend fun clearCache() { + } + override suspend fun logout() { delay(100) logoutFailure?.let { throw it } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt index 816bfc572a..81fa3b677c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.flowOf val A_OIDC_DATA = OidcDetails(url = "a-url") class FakeAuthenticationService : MatrixAuthenticationService { - private var homeserver = MutableStateFlow(null) + private val homeserver = MutableStateFlow(null) private var oidcError: Throwable? = null private var oidcCancelError: Throwable? = null private var loginError: Throwable? = null diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index 9038e03611..c5b7f1ed44 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -26,16 +26,17 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import okhttp3.OkHttpClient import javax.inject.Inject +import javax.inject.Provider class LoggedInImageLoaderFactory @Inject constructor( @ApplicationContext private val context: Context, private val matrixClient: MatrixClient, - private val okHttpClient: OkHttpClient, + private val okHttpClient: Provider, ) : ImageLoaderFactory { override fun newImageLoader(): ImageLoader { return ImageLoader .Builder(context) - .okHttpClient(okHttpClient) + .okHttpClient { okHttpClient.get() } .components { // Add gif support if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -54,12 +55,12 @@ class LoggedInImageLoaderFactory @Inject constructor( class NotLoggedInImageLoaderFactory @Inject constructor( @ApplicationContext private val context: Context, - private val okHttpClient: OkHttpClient, + private val okHttpClient: Provider, ) : ImageLoaderFactory { override fun newImageLoader(): ImageLoader { return ImageLoader .Builder(context) - .okHttpClient(okHttpClient) + .okHttpClient { okHttpClient.get() } .build() } } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index e4cb389d94..5d8e0bfd1c 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -43,6 +43,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone +import timber.log.Timber import java.util.Locale class RoomListScreen( @@ -106,8 +107,10 @@ class RoomListScreen( ) DisposableEffect(Unit) { + Timber.w("Start sync!") matrixClient.startSync() onDispose { + Timber.w("Stop sync!") matrixClient.stopSync() } } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 57b4a846e2..db838e0f1f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d806cbab0f26fb4f471adb5fbecfc600603a7651c5391501d42b13b23617a4c -size 29301 +oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae +size 35345 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..db838e0f1f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9597821bbe6b65693470b40e5f570cf318821d2dbf5bdf525d447daff7d352ae +size 35345 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index d8d2dbc8d1..9786515e1c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d04eb5d2ebb8b740edcaac00912994e8c164e7b5083595d4085d350af0ca6c33 -size 28482 +oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0 +size 34308 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9786515e1c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61880d7b08cc92743a12a74246495039b76e1e80e2704839f726329efb6958a0 +size 34308 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png index 719089d8e9..cf208695d7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04e83ead8c23ec9160eb9cfd656aa5e9bf8c302dcf435d00b78aa2eba3243f0d -size 64181 +oid sha256:05dc60cbfa8de0e27acecc5d44f7e1841d15f1cdcb749dcf23995aa49d40d924 +size 64174 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png index dcdf364d71..9d69f48d15 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b59df47ab2ad44752eb3441ed117fff517e42c2c08f5e0c6402168223f35daef -size 53042 +oid sha256:dc5b5b7a08b61201bde775880eadd40dc51773fe616209c85c143fc703091cef +size 53024 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png index ef99f706a2..cfb41488ee 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:002fa211918a6d41c993339b30261fb8b564df442756bafe9f8866d2ecf51c81 -size 54256 +oid sha256:51f3ee04917b0725bc0a721ac770636b7c273c1ef6f800c5ff3087ea59906396 +size 54267 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png index bfc84aa2bf..2c2e946b6d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec598fdc25acf4300e9e44a7b4913e3b9dd881cd27222393047421cd8c361217 -size 54722 +oid sha256:05e6a676f742d70c75d1e4ed35667aadbf22e0d796ef165b5265e8889c5bd062 +size 54715 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png index c64bff1e9f..dd3e5c1a5e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df7c6f78b6a54d1444388aafdd76c8c00ba15be688e2ee10f0582339e74e2499 -size 67872 +oid sha256:51fb6dadd1af1bc140877f205d57270ff67a46a3b4c4508ddcdf11e2402f60e4 +size 67859 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png index d4c96e56b4..85103e6e4d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:872c9b5285190577292485c2c0439ceb2259b68740591f1cad7129f322b089b3 -size 57677 +oid sha256:1471ebd62c5514c04bb943e83ae1f2bf51edad409da5fb9c8f6613fbae09d333 +size 57679 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png index 29f39d40c7..b101fabb04 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3bdab2714bd1b232b255fbe0799209da5f40a390384fb21f56bbc1788032129 -size 64512 +oid sha256:c164ad24729e25e5b4a1031bb6bb452516e9172b65d867d584efa63e5a095355 +size 64505 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png index a12afbc7c9..625da9d324 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9a1a5bb3c04fb601b84a2b1373544e8eba94f688d938a990308a0e788d96445 -size 61182 +oid sha256:e651c80d07b153dde903f427b4e41e3475c51b524d877ffe55b265ffaac8c32a +size 61197 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png index 79ebe73047..9e9b406f63 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7ee7551b233428723d79b099d7bac4634188a3ac120ff3389b2ed33625dcc94 -size 51252 +oid sha256:cb6a8ad69606bac57df9e1c758756eaf09e887875989546c8aa628207926b79e +size 51274 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png index d0c208aa7c..f70b484141 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48e7307ffc1d8f7f7a885e67851314c3b135e74a4268ff5fede59758f91a3358 -size 51926 +oid sha256:75eab912dfea0e0eac42742dc131d1630270ac91bf840986be56a755765ee18a +size 51917 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png index 2a02579eb9..faf29851e2 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:632f982253a6e6f0101fd60a0ef48601cc2c06268ab3d66db084571a5861a52c -size 52417 +oid sha256:d0d5bee6b1b4b6069a55c0f297a864979b7c1f1f212bc5bccf8806bd82004abc +size 52429 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png index 43fe21a48a..73cdbe63be 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c49a225d80fe15170e6444271b63324f67099bb733eb1c4b989508a4d0d678ef -size 64502 +oid sha256:4c2ce12fd0e6edff9ab4a468ec9f3eab2a206b7c4c51b26fa9fa7bf5a5efdc63 +size 64472 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png index 4975863ccb..7c14d499d2 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8612af1823d149e503e5d47814ecb171a4b2a4be19a243751c89034a1fdb30d -size 55076 +oid sha256:2e000d7940a2fae0a350d508df8b5c95a27f76f60a865cd7372b8a32d89318f6 +size 55082 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png index 885fc65fb2..e34c095393 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40ecdf8314109687437223860b5e28847d0cb8ba7d546829de5f061640e58315 -size 61500 +oid sha256:86cca8af9ab505cd9cd1ae4a941d5053f2d3155eb033ed1121e7d15f92a6cb43 +size 61509 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png index d818c6f2a0..21db86fe46 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_Preferences_PreferenceTextPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fc465aa6658ace0327804f04e329cae12bf358daca27e48a8ae6bd516752a9c -size 12843 +oid sha256:197b1b5fa33ba31f4e47f70b12e4b6eaf7fb3ea30368e96b7dec08f37bdeb62c +size 28185