From a76b55e5808c4f8dcdcf7d6e000e14dbf029269d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Apr 2026 17:55:56 +0200 Subject: [PATCH 1/9] Add a way to tweak MAS url. --- enterprise | 2 +- .../features/enterprise/api/EnterpriseService.kt | 1 + .../enterprise/api/SessionEnterpriseService.kt | 1 + .../enterprise/impl/DefaultEnterpriseService.kt | 2 +- .../enterprise/impl/DefaultSessionEnterpriseService.kt | 1 + .../features/enterprise/test/FakeEnterpriseService.kt | 5 +++++ .../enterprise/test/FakeSessionEnterpriseService.kt | 5 +++++ .../linknewdevice/impl/LinkNewDeviceFlowNode.kt | 10 ++++++++-- .../impl/DefaultLinkNewDeviceEntryPointTest.kt | 2 ++ .../preferences/impl/root/PreferencesRootPresenter.kt | 8 +++++++- .../impl/root/PreferencesRootPresenterTest.kt | 9 ++++++++- features/securebackup/impl/build.gradle.kts | 1 + .../securebackup/impl/reset/ResetIdentityFlowNode.kt | 5 ++++- libraries/matrix/impl/build.gradle.kts | 1 + .../impl/auth/RustMatrixAuthenticationService.kt | 8 ++++++++ .../libraries/wellknown/api/ElementWellKnown.kt | 1 + .../wellknown/impl/InternalElementWellKnown.kt | 2 ++ .../element/android/libraries/wellknown/impl/Mapper.kt | 1 + .../android/features/wellknown/test/Fixtures.kt | 2 ++ 19 files changed, 60 insertions(+), 7 deletions(-) diff --git a/enterprise b/enterprise index cdde60c158..1ed9a7e8b4 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit cdde60c158ecd0987a3ba6fd79a4617551aff463 +Subproject commit 1ed9a7e8b4406a5e6d12f603cfc2083e84f8576f diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 65fe3fe087..92d8b9b646 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow interface EnterpriseService { val isEnterpriseBuild: Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean + suspend fun tweakMasUrl(url: String, homeserver: String): String fun defaultHomeserverList(): List suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt index 6bd6c78de5..f87dc743e8 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt @@ -10,6 +10,7 @@ package io.element.android.features.enterprise.api interface SessionEnterpriseService { suspend fun isElementCallAvailable(): Boolean + suspend fun tweakMasUrl(url: String): String suspend fun init() } diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 932d082fd9..6e3ed5d3cc 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -23,7 +23,7 @@ class DefaultEnterpriseService : EnterpriseService { override val isEnterpriseBuild = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false - + override suspend fun tweakMasUrl(url: String, homeserver: String) = url override fun defaultHomeserverList(): List = emptyList() override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt index 3441063a8a..9aafcd343c 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt @@ -15,5 +15,6 @@ import io.element.android.libraries.di.SessionScope @ContributesBinding(SessionScope::class) class DefaultSessionEnterpriseService : SessionEnterpriseService { override suspend fun init() = Unit + override suspend fun tweakMasUrl(url: String): String = url override suspend fun isElementCallAvailable(): Boolean = true } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index 3c17a4de7c..805c75be6a 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -30,6 +30,7 @@ class FakeEnterpriseService( private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() }, + private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() }, ) : EnterpriseService { private val brandColorState = MutableStateFlow(initialBrandColor) private val semanticColorsState = MutableStateFlow(initialSemanticColors) @@ -38,6 +39,10 @@ class FakeEnterpriseService( isEnterpriseUserResult(sessionId) } + override suspend fun tweakMasUrl(url: String, homeserver: String): String = simulateLongTask { + tweakMasUrlResult(url, homeserver) + } + override fun defaultHomeserverList(): List { return defaultHomeserverListResult() } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt index 3914c60155..0bcad13033 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt @@ -14,10 +14,15 @@ import io.element.android.tests.testutils.simulateLongTask class FakeSessionEnterpriseService( private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, + private val tweakMasUrlResult: (String) -> String = { lambdaError() }, ) : SessionEnterpriseService { override suspend fun init() { } + override suspend fun tweakMasUrl(url: String): String = simulateLongTask { + tweakMasUrlResult(url) + } + override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { isElementCallAvailableResult() } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt index 4ccdb6b387..9674d213f9 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt @@ -26,6 +26,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint import io.element.android.features.linknewdevice.impl.screens.confirmation.CodeConfirmationNode import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode @@ -65,6 +66,7 @@ class LinkNewDeviceFlowNode( private val sessionCoroutineScope: CoroutineScope, private val linkNewMobileHandler: LinkNewMobileHandler, private val linkNewDesktopHandler: LinkNewDesktopHandler, + private val sessionEnterpriseService: SessionEnterpriseService, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -301,8 +303,12 @@ class LinkNewDeviceFlowNode( } } - private fun navigateToBrowser(url: String) { - activity?.openUrlInChromeCustomTab(null, darkTheme, url) + private suspend fun navigateToBrowser(url: String) { + activity?.openUrlInChromeCustomTab( + session = null, + darkTheme = darkTheme, + url = sessionEnterpriseService.tweakMasUrl(url), + ) } @Composable diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt index 2957a89495..1b2af8f4c3 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt @@ -11,6 +11,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.lambda.lambdaError @@ -37,6 +38,7 @@ class DefaultLinkNewDeviceEntryPointTest { sessionCoroutineScope = backgroundScope, linkNewMobileHandler = LinkNewMobileHandler(client), linkNewDesktopHandler = LinkNewDesktopHandler(client), + sessionEnterpriseService = FakeSessionEnterpriseService(), ) } val callback: LinkNewDeviceEntryPoint.Callback = object : LinkNewDeviceEntryPoint.Callback { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 43da681a37..f407149007 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.rageshake.api.RageshakeFeatureAvailability @@ -55,6 +56,7 @@ class PreferencesRootPresenter( private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val featureFlagService: FeatureFlagService, private val sessionStore: SessionStore, + private val sessionEnterpriseService: SessionEnterpriseService, ) : Presenter { @Composable override fun present(): PreferencesRootState { @@ -158,6 +160,10 @@ class PreferencesRootPresenter( private fun CoroutineScope.initAccountManagementUrl( accountManagementUrl: MutableState, ) = launch { - accountManagementUrl.value = matrixClient.getAccountManagementUrl(null).getOrNull() + accountManagementUrl.value = matrixClient.getAccountManagementUrl(null) + .getOrNull() + ?.let { + sessionEnterpriseService.tweakMasUrl(it) + } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index d324f5eb3a..a6861be12d 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -12,6 +12,8 @@ package io.element.android.features.preferences.impl.root import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.SessionEnterpriseService +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.rageshake.api.RageshakeFeatureAvailability @@ -65,6 +67,9 @@ class PreferencesRootPresenterTest { ) createPresenter( matrixClient = matrixClient, + sessionEnterpriseService = FakeSessionEnterpriseService( + tweakMasUrlResult = { "tweaked $it" }, + ), ).test { val initialState = awaitItem() assertThat(initialState.myUser).isEqualTo( @@ -100,7 +105,7 @@ class PreferencesRootPresenterTest { val finalState = awaitItem() accountManagementUrlResult.assertions().isCalledOnce() .with(value(null)) - assertThat(finalState.accountManagementUrl).isEqualTo("null url") + assertThat(finalState.accountManagementUrl).isEqualTo("tweaked null url") } } @@ -327,6 +332,7 @@ class PreferencesRootPresenterTest { indicatorService: IndicatorService = FakeIndicatorService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), sessionStore: SessionStore = InMemorySessionStore(), + sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(), ) = PreferencesRootPresenter( matrixClient = matrixClient, sessionVerificationService = sessionVerificationService, @@ -339,5 +345,6 @@ class PreferencesRootPresenterTest { rageshakeFeatureAvailability = rageshakeFeatureAvailability, featureFlagService = featureFlagService, sessionStore = sessionStore, + sessionEnterpriseService = sessionEnterpriseService, ) } diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index 82f30fa5ce..54d87ef22e 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -28,6 +28,7 @@ setupDependencyInjection() dependencies { implementation(projects.appconfig) + implementation(projects.features.enterprise.api) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt index 4e2284890f..7dd6bf632a 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt @@ -25,6 +25,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab @@ -53,6 +54,7 @@ class ResetIdentityFlowNode( private val resetIdentityFlowManager: ResetIdentityFlowManager, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, + private val sessionEnterpriseService: SessionEnterpriseService, ) : BaseFlowNode( backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap), buildContext = buildContext, @@ -125,7 +127,8 @@ class ResetIdentityFlowNode( } is IdentityOAuthResetHandle -> { Timber.d("Launching reset confirmation in MAS") - activity.openUrlInChromeCustomTab(null, darkTheme, handle.url) + val url = sessionEnterpriseService.tweakMasUrl(handle.url) + activity.openUrlInChromeCustomTab(null, darkTheme, url) Timber.d("Starting resetOAuth") resetJob = launch { handle.resetOAuth() } resetJob?.invokeOnCompletion { Timber.d("resetOAuth ended") } diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 67386cc592..7dcc835781 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(projects.libraries.rustlsTls) implementation(projects.appconfig) + implementation(projects.features.enterprise.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.di) implementation(projects.libraries.featureflag.api) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 79793e4e77..6cd30d4f70 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.auth import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -66,6 +67,7 @@ class RustMatrixAuthenticationService( private val rustMatrixClientFactory: RustMatrixClientFactory, private val passphraseGenerator: PassphraseGenerator, private val oAuthConfigurationProvider: OAuthConfigurationProvider, + private val enterpriseService: EnterpriseService, ) : MatrixAuthenticationService { // Any existing Element Classic session that we want to try to import secrets from during login. private var elementClassicSession: ElementClassicSession? = null @@ -269,6 +271,12 @@ class RustMatrixAuthenticationService( additionalScopes = emptyList(), ) val url = oAuthAuthorizationData.loginUrl() + .let { + enterpriseService.tweakMasUrl( + url = it, + homeserver = client.server() ?: client.homeserver(), + ) + } pendingOAuthAuthorizationData = oAuthAuthorizationData OAuthDetails(url) }.mapFailure { failure -> diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt index 134a9bcdb5..4c1c476c7a 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -14,4 +14,5 @@ data class ElementWellKnown( val rageshakeUrl: String?, val brandColor: String?, val notificationSound: String?, + val identityProviderAppScheme: String?, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index d4661d1be0..2e2b5de16f 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -32,4 +32,6 @@ data class InternalElementWellKnown( val brandColor: String? = null, @SerialName("notification_sound") val notificationSound: String? = null, + @SerialName("idp_app_scheme") + val identityProviderAppScheme: String? = null, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt index c7ca088e68..41ed54d7db 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -16,4 +16,5 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown( rageshakeUrl = rageshakeUrl, brandColor = brandColor, notificationSound = notificationSound, + identityProviderAppScheme = identityProviderAppScheme, ) diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt index 7457aafc98..ae7d1c629c 100644 --- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt @@ -16,10 +16,12 @@ fun anElementWellKnown( rageshakeUrl: String? = null, brandColor: String? = null, notificationSound: String? = null, + identityProviderAppScheme: String? = null, ) = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, rageshakeUrl = rageshakeUrl, brandColor = brandColor, notificationSound = notificationSound, + identityProviderAppScheme = identityProviderAppScheme, ) From c1e908a8e64e2699bc70a05265bdfd3122cfa876 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2026 09:19:41 +0200 Subject: [PATCH 2/9] Fix tests. --- libraries/matrix/impl/build.gradle.kts | 1 + .../matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt | 4 ++++ .../wellknown/impl/DefaultSessionWellknownRetrieverTest.kt | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 7dcc835781..32bbeb082a 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(libs.kotlinx.collections.immutable) testCommonDependencies(libs) + testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt index bff2711bc5..7f422acfcf 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt @@ -9,6 +9,8 @@ package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.matrix.impl.ClientBuilderProvider import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory @@ -50,6 +52,7 @@ class RustMatrixAuthenticationServiceTest { private fun TestScope.createRustMatrixAuthenticationService( sessionStore: SessionStore = InMemorySessionStore(), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), ): RustMatrixAuthenticationService { val baseDirectory = File("/base") val cacheDirectory = File("/cache") @@ -68,6 +71,7 @@ class RustMatrixAuthenticationServiceTest { buildMeta = aBuildMeta(), oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), ), + enterpriseService = enterpriseService, ) } } diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index faad139a36..8a230c0317 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -36,6 +36,7 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = null, brandColor = null, notificationSound = null, + identityProviderAppScheme = null, ) ) ) @@ -53,7 +54,8 @@ class DefaultSessionWellknownRetrieverTest { "enforce_element_pro": true, "rageshake_url": "a_rageshake_url", "brand_color": "#FF0000", - "notification_sound": "a_notification_sound.flac" + "notification_sound": "a_notification_sound.flac", + "idp_app_scheme": "an_app_scheme" }""".trimIndent().toByteArray() ) } @@ -66,6 +68,7 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = "a_rageshake_url", brandColor = "#FF0000", notificationSound = "a_notification_sound.flac", + identityProviderAppScheme = "an_app_scheme", ) ) ) @@ -93,6 +96,7 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = "a_rageshake_url", brandColor = null, notificationSound = null, + identityProviderAppScheme = null, ) ) ) From 29e0a08dd93e4df5d7fce4fbf456cab738b274c4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2026 16:39:07 +0200 Subject: [PATCH 3/9] Add a CacheStore module. --- libraries/cachestore/api/build.gradle.kts | 13 ++++ .../libraries/cachestore/api/CacheData.kt | 13 ++++ .../libraries/cachestore/api/CacheStore.kt | 14 ++++ libraries/cachestore/impl/build.gradle.kts | 48 +++++++++++++ .../cachestore/impl/CacheDataMapper.kt | 27 +++++++ .../cachestore/impl/DatabaseCacheStore.kt | 36 ++++++++++ .../cachestore/impl/di/CacheStoreModule.kt | 43 +++++++++++ .../impl/src/main/sqldelight/databases/1.db | Bin 0 -> 12288 bytes .../android/libraries/cachestore/CacheData.sq | 26 +++++++ .../impl/DatabaseCacheStoreTest.kt | 68 ++++++++++++++++++ .../libraries/sessionstorage/impl/Fixtures.kt | 21 ++++++ libraries/cachestore/test/build.gradle.kts | 17 +++++ .../sessionstorage/test/CacheData.kt | 18 +++++ .../sessionstorage/test/InMemoryCacheStore.kt | 29 ++++++++ .../kotlin/extension/DependencyHandleScope.kt | 1 + 15 files changed, 374 insertions(+) create mode 100644 libraries/cachestore/api/build.gradle.kts create mode 100644 libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.kt create mode 100644 libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt create mode 100644 libraries/cachestore/impl/build.gradle.kts create mode 100644 libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt create mode 100644 libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt create mode 100644 libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt create mode 100644 libraries/cachestore/impl/src/main/sqldelight/databases/1.db create mode 100644 libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq create mode 100644 libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt create mode 100644 libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt create mode 100644 libraries/cachestore/test/build.gradle.kts create mode 100644 libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.kt create mode 100644 libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt diff --git a/libraries/cachestore/api/build.gradle.kts b/libraries/cachestore/api/build.gradle.kts new file mode 100644 index 0000000000..0e03bb5136 --- /dev/null +++ b/libraries/cachestore/api/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.cachestore.api" +} diff --git a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.kt b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.kt new file mode 100644 index 0000000000..a448ba7df8 --- /dev/null +++ b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.cachestore.api + +data class CacheData( + val value: String, + val updatedAt: Long, +) diff --git a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt new file mode 100644 index 0000000000..f16a663ed7 --- /dev/null +++ b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.cachestore.api + +interface CacheStore { + suspend fun storeData(key: String, data: CacheData) + suspend fun getData(key: String): CacheData? + suspend fun deleteData(key: String) +} diff --git a/libraries/cachestore/impl/build.gradle.kts b/libraries/cachestore/impl/build.gradle.kts new file mode 100644 index 0000000000..f0c7ba237c --- /dev/null +++ b/libraries/cachestore/impl/build.gradle.kts @@ -0,0 +1,48 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-library") + alias(libs.plugins.sqldelight) +} + +android { + namespace = "io.element.android.libraries.cachestore.impl" +} + +setupDependencyInjection() + +dependencies { + implementation(projects.libraries.androidutils) + implementation(projects.libraries.core) + implementation(projects.libraries.encryptedDb) + api(projects.libraries.cachestore.api) + implementation(libs.sqldelight.driver.android) + implementation(libs.sqlcipher) + implementation(libs.sqlite) + implementation(projects.libraries.di) + implementation(libs.sqldelight.coroutines) + + testCommonDependencies(libs) + testImplementation(libs.sqldelight.driver.jvm) +} + +sqldelight { + databases { + create("CacheDatabase") { + // https://sqldelight.github.io/sqldelight/2.1.0/android_sqlite/migrations/ + // To generate a .db file from your latest schema, run this task + // ./gradlew generateDebugCacheDatabaseSchema + // Test migration by running + // ./gradlew verifySqlDelightMigration + schemaOutputDirectory = File("src/main/sqldelight/databases") + verifyMigrations = true + } + } +} diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt new file mode 100644 index 0000000000..7e67b6de72 --- /dev/null +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.cachestore.impl + +import io.element.android.libraries.cachestore.api.CacheData +import java.util.Date +import io.element.android.libraries.cachestore.CacheData as DbCacheData + +internal fun CacheData.toDbModel(key: String): DbCacheData { + return DbCacheData( + key = key, + value_ = value, + updatedAt = updatedAt.time, + ) +} + +internal fun DbCacheData.toApiModel(): CacheData { + return CacheData( + value = value_, + updatedAt = Date(updatedAt), + ) +} diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt new file mode 100644 index 0000000000..126387da11 --- /dev/null +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.cachestore.impl + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DatabaseCacheStore( + private val database: CacheDatabase, +) : CacheStore { + override suspend fun getData(key: String): CacheData? { + return database.cacheDataQueries.selectData(key) + .executeAsOneOrNull() + ?.toApiModel() + } + + override suspend fun storeData(key: String, data: CacheData) { + database.cacheDataQueries.insertData( + data.toDbModel(key) + ) + } + + override suspend fun deleteData(key: String) { + database.cacheDataQueries.deleteData(key) + } +} diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt new file mode 100644 index 0000000000..05fa3d9d97 --- /dev/null +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.cachestore.impl.di + +import android.content.Context +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.cachestore.impl.CacheDatabase +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.encrypteddb.SqlCipherDriverFactory +import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider + +@BindingContainer +@ContributesTo(AppScope::class) +object CacheStoreModule { + @Provides + @SingleIn(AppScope::class) + fun provideCacheDatabase( + @ApplicationContext context: Context, + ): CacheDatabase { + val name = "cache_database" + val secretFile = context.getDatabasePath("$name.key") + + // Make sure the parent directory of the key file exists, otherwise it will crash in older Android versions + val parentDir = secretFile.parentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + } + + val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile) + val driver = SqlCipherDriverFactory(passphraseProvider) + .create(CacheDatabase.Schema, "$name.db", context) + return CacheDatabase(driver) + } +} diff --git a/libraries/cachestore/impl/src/main/sqldelight/databases/1.db b/libraries/cachestore/impl/src/main/sqldelight/databases/1.db new file mode 100644 index 0000000000000000000000000000000000000000..8e4b0cac72d8a31cf1c7038ebce700b66768eaa2 GIT binary patch literal 12288 zcmeI#&r8EF6bJC6ib`R02Vuv);{+)T{{ia^GOU}{2<}v(jX12%AL-QN{s8|h|0ltV zNo4^~I|}3b;H7y@TAEK!@BA#A7)>|R=X+&nkF^=+Y@diR#%-?IT!gFF7lpmLdRcIm ze0;Z}FBU`zi<0Pb*#lUI00bZa0SG_<0uX=z1Rwwb2>idme4B4};+W50jk>CIUtQno zLuHho1pQp3Qjkm!GC}?}bwjdor{776OG(3#&5JCf(|mA}=3_b*gQ5Rl)!&PAK>z{}fB*y_009U<00Izz00bI= EA429uc>n+a literal 0 HcmV?d00001 diff --git a/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq new file mode 100644 index 0000000000..4683d9aa7d --- /dev/null +++ b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq @@ -0,0 +1,26 @@ +-------------------------------------------------------------------- +-- Current version of the DB is the highest value of filename +-- in the folder `sqldelight/databases`. +-- +-- When upgrading the schema, you have to create a file .sqm in the +-- `sqldelight/databases` folder and run the following task to +-- generate a .db file using the latest schema +-- > ./gradlew generateDebugCacheDatabaseSchema +-------------------------------------------------------------------- + +CREATE TABLE CacheData ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + updatedAt INTEGER NOT NULL +); + + +selectData: +SELECT * FROM CacheData WHERE key = ?; + +-- insert or update data by key +insertData: +INSERT INTO CacheData VALUES ? ON CONFLICT(key) DO UPDATE SET value = excluded.value, updatedAt = excluded.updatedAt; + +deleteData: +DELETE FROM CacheData WHERE key = ?; diff --git a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt new file mode 100644 index 0000000000..a2216daa06 --- /dev/null +++ b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.sessionstorage.impl + +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.impl.CacheDatabase +import io.element.android.libraries.cachestore.impl.DatabaseCacheStore +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import io.element.android.libraries.cachestore.CacheData as DbCacheData + +private const val A_KEY = "aKey" +private const val A_DATA_1 = "aData1" +private const val A_DATA_2 = "aData2" + +class DatabaseCacheStoreTest { + private lateinit var database: CacheDatabase + private lateinit var databaseCacheStore: DatabaseCacheStore + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + // Initialise in memory SQLite driver + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + CacheDatabase.Schema.create(driver) + + database = CacheDatabase(driver) + databaseCacheStore = DatabaseCacheStore( + database = database, + ) + } + + @Test + fun `storeData persists the CacheData into the DB, deleteData deletes it`() = runTest { + // Assert that no data is stored for the key + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + // Store data + databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_1, 1)) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo( + DbCacheData( + key = A_KEY, + value_ = A_DATA_1, + updatedAt = 1, + ) + ) + // Update data + databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_2, 2)) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo( + DbCacheData( + key = A_KEY, + value_ = A_DATA_2, + updatedAt = 2, + ) + ) + // Delete data + databaseCacheStore.deleteData(A_KEY) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + } +} diff --git a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt new file mode 100644 index 0000000000..3dca9efaf3 --- /dev/null +++ b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.sessionstorage.impl + +import io.element.android.libraries.cachestore.CacheData +import java.util.Date + +internal fun aCacheData( + key: String = "aKey", + value: String = "aValue", + updatedAt: Date = Date(), +) = CacheData( + key = key, + value_ = value, + updatedAt = updatedAt.time, +) diff --git a/libraries/cachestore/test/build.gradle.kts b/libraries/cachestore/test/build.gradle.kts new file mode 100644 index 0000000000..7ad2ac48d9 --- /dev/null +++ b/libraries/cachestore/test/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.cachestore.test" +} + +dependencies { + implementation(projects.libraries.cachestore.api) +} diff --git a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.kt b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.kt new file mode 100644 index 0000000000..30633e8ff9 --- /dev/null +++ b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.sessionstorage.test + +import io.element.android.libraries.cachestore.api.CacheData + +fun aCacheData( + value: String = "aValue", + updatedAt: Long = 0, +) = CacheData( + value = value, + updatedAt = updatedAt, +) diff --git a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt new file mode 100644 index 0000000000..b0f77062ba --- /dev/null +++ b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.sessionstorage.test + +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore + +class InMemoryCacheStore( + initialData: Map = emptyMap(), +) : CacheStore { + val dataMap = initialData.toMutableMap() + + override suspend fun storeData(key: String, data: CacheData) { + dataMap[key] = data + } + + override suspend fun getData(key: String): CacheData? { + return dataMap[key] + } + + override suspend fun deleteData(key: String) { + dataMap.remove(key) + } +} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index f832136083..3e01ffc25f 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -104,6 +104,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) + implementation(project(":libraries:cachestore:impl")) implementation(project(":libraries:session-storage:impl")) implementation(project(":libraries:mediapickers:impl")) implementation(project(":libraries:mediaupload:impl")) From 8512279d9619d97370984e1dd426cee5d6e0e2ed Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2026 22:05:14 +0200 Subject: [PATCH 4/9] Add a cache for Element .well-known file. --- libraries/wellknown/impl/build.gradle.kts | 4 + .../impl/DefaultSessionWellknownRetriever.kt | 48 ++++++- .../DefaultSessionWellknownRetrieverTest.kt | 132 ++++++++++++++++-- 3 files changed, 173 insertions(+), 11 deletions(-) diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts index f803eeec3c..1e2c4d7d61 100644 --- a/libraries/wellknown/impl/build.gradle.kts +++ b/libraries/wellknown/impl/build.gradle.kts @@ -33,9 +33,13 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.network) + implementation(projects.libraries.cachestore.api) + implementation(projects.services.toolbox.api) testCommonDependencies(libs) testImplementation(libs.coroutines.core) + testImplementation(projects.libraries.cachestore.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.wellknown.test) testImplementation(projects.services.toolbox.test) } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt index 3bcf9bf573..a0223e93cc 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -10,29 +10,70 @@ package io.element.android.libraries.wellknown.impl import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.androidutils.json.JsonProvider +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.SessionWellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import timber.log.Timber @ContributesBinding(SessionScope::class) class DefaultSessionWellknownRetriever( private val matrixClient: MatrixClient, private val json: JsonProvider, + private val cacheStore: CacheStore, + private val systemClock: SystemClock, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, ) : SessionWellknownRetriever { private val domain by lazy { matrixClient.userIdServerName() } override suspend fun getElementWellKnown(): WellknownRetrieverResult { val url = "https://$domain/.well-known/element/element.json" + val cacheData = cacheStore.getData(url) + if (cacheData != null) { + Timber.d("Element .well-known data retrieved from cache for $domain") + // If the cache is outdated, trigger a refresh in background but still return the cached value + if (systemClock.epochMillis() > cacheData.updatedAt + CACHE_VALIDITY_MILLIS) { + sessionCoroutineScope.launch { + fetchElementWellKnown(url) + } + } + try { + val parsed = json().decodeFromString(cacheData.value).map() + return WellknownRetrieverResult.Success(parsed) + } catch (e: Exception) { + Timber.e(e, "Failed to parse cached Element .well-known data for $domain, deleting cache") + cacheStore.deleteData(url) + } + } + + return fetchElementWellKnown(url) + } + + private suspend fun fetchElementWellKnown(url: String): WellknownRetrieverResult { return matrixClient .getUrl(url) .mapCatchingExceptions { val data = String(it) - json().decodeFromString(data).map() + val parsed = json().decodeFromString(data).map() + // Also store in cache, if valid + cacheStore.storeData( + key = url, + data = CacheData( + value = data, + updatedAt = systemClock.epochMillis(), + ) + ) + parsed } .toWellknownRetrieverResult() } @@ -51,4 +92,9 @@ class DefaultSessionWellknownRetriever( } } ) + + companion object { + // 1 day + private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L + } } diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index 8a230c0317..2824a7e112 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -6,16 +6,30 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.libraries.wellknown.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.libraries.androidutils.json.DefaultJsonProvider +import io.element.android.libraries.androidutils.json.JsonProvider +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.services.toolbox.api.systemclock.SystemClock +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -49,14 +63,7 @@ class DefaultSessionWellknownRetrieverTest { val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { Result.success( - """{ - "registration_helper_url": "a_registration_url", - "enforce_element_pro": true, - "rageshake_url": "a_rageshake_url", - "brand_color": "#FF0000", - "notification_sound": "a_notification_sound.flac", - "idp_app_scheme": "an_app_scheme" - }""".trimIndent().toByteArray() + WELLKNOWN_CONTENT.toByteArray() ) } ) @@ -127,13 +134,118 @@ class DefaultSessionWellknownRetrieverTest { assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) } - private fun createDefaultSessionWellknownRetriever( + @Test + fun `get element wellknown hitting cache`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { lambdaError() }, + cacheStore = InMemoryCacheStore( + initialData = mapOf( + WELLKNOWN_URL to CacheData( + value = WELLKNOWN_CONTENT, + updatedAt = A_FAKE_TIMESTAMP, + ) + ) + ) + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + brandColor = "#FF0000", + notificationSound = "a_notification_sound.flac", + identityProviderAppScheme = "an_app_scheme", + ) + ) + ) + } + + @Test + fun `get element wellknown hitting cache containing invalid json`() = runTest { + val cacheStore = InMemoryCacheStore( + initialData = mapOf( + WELLKNOWN_URL to CacheData( + value = WELLKNOWN_CONTENT, + updatedAt = A_FAKE_TIMESTAMP, + ) + ) + ) + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success("{}".toByteArray()) + }, + cacheStore = cacheStore, + jsonProvider = JsonProvider { throw Exception("Failed to parse JSON") } + ) + assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) + // Ensure that the cache is deleted after the failure to parse it + assertThat(cacheStore.dataMap).isEmpty() + } + + @Test + fun `get element wellknown hitting outdated cache`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success("{}".toByteArray()) + }, + cacheStore = InMemoryCacheStore( + initialData = mapOf( + WELLKNOWN_URL to CacheData( + value = WELLKNOWN_CONTENT, + updatedAt = 0L, + ) + ), + ), + // 3 days later, so the cache is outdated + systemClock = FakeSystemClock(3 * 24 * 60 * 60 * 1000L) + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + brandColor = "#FF0000", + notificationSound = "a_notification_sound.flac", + identityProviderAppScheme = "an_app_scheme", + ) + ) + ) + // Next call returns the updated value + runCurrent() + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown() + ) + ) + } + + private fun TestScope.createDefaultSessionWellknownRetriever( getUrlLambda: (String) -> Result, + jsonProvider: JsonProvider = DefaultJsonProvider(), + cacheStore: CacheStore = InMemoryCacheStore(), + systemClock: SystemClock = FakeSystemClock(), ) = DefaultSessionWellknownRetriever( matrixClient = FakeMatrixClient( userIdServerNameLambda = { "user.domain.org" }, getUrlLambda = getUrlLambda, ), - json = DefaultJsonProvider(), + json = jsonProvider, + cacheStore = cacheStore, + systemClock = systemClock, + sessionCoroutineScope = backgroundScope, ) + + companion object { + private const val WELLKNOWN_URL = "https://user.domain.org/.well-known/element/element.json" + private const val WELLKNOWN_CONTENT = """{ + "registration_helper_url": "a_registration_url", + "enforce_element_pro": true, + "rageshake_url": "a_rageshake_url", + "brand_color": "#FF0000", + "notification_sound": "a_notification_sound.flac", + "idp_app_scheme": "an_app_scheme" + }""" + } } From 35a3a0ba063cf85f1cbe3bc4d0b1a8d8955c0066 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2026 22:43:42 +0200 Subject: [PATCH 5/9] Fix compilation issue. --- .../android/libraries/cachestore/impl/CacheDataMapper.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt index 7e67b6de72..2a33ce4fda 100644 --- a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt @@ -8,20 +8,19 @@ package io.element.android.libraries.cachestore.impl import io.element.android.libraries.cachestore.api.CacheData -import java.util.Date import io.element.android.libraries.cachestore.CacheData as DbCacheData internal fun CacheData.toDbModel(key: String): DbCacheData { return DbCacheData( key = key, value_ = value, - updatedAt = updatedAt.time, + updatedAt = updatedAt, ) } internal fun DbCacheData.toApiModel(): CacheData { return CacheData( value = value_, - updatedAt = Date(updatedAt), + updatedAt = updatedAt, ) } From 3dd2a60b3a1994a6dbd8f246460604bb71ba08e2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2026 22:51:54 +0200 Subject: [PATCH 6/9] Do not use generic Exception --- .../wellknown/impl/DefaultSessionWellknownRetrieverTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index 2824a7e112..5b1cebd6cf 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -176,7 +176,7 @@ class DefaultSessionWellknownRetrieverTest { Result.success("{}".toByteArray()) }, cacheStore = cacheStore, - jsonProvider = JsonProvider { throw Exception("Failed to parse JSON") } + jsonProvider = JsonProvider { error("Failed to parse JSON") } ) assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) // Ensure that the cache is deleted after the failure to parse it From c8773c890f2fe88d7075cc5f8ea37c6eb1be44c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2026 08:49:56 +0200 Subject: [PATCH 7/9] Fix SQL query --- .../io/element/android/libraries/cachestore/CacheData.sq | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq index 4683d9aa7d..aa8bacf90b 100644 --- a/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq +++ b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq @@ -18,9 +18,8 @@ CREATE TABLE CacheData ( selectData: SELECT * FROM CacheData WHERE key = ?; --- insert or update data by key insertData: -INSERT INTO CacheData VALUES ? ON CONFLICT(key) DO UPDATE SET value = excluded.value, updatedAt = excluded.updatedAt; +INSERT OR REPLACE INTO CacheData VALUES ?; deleteData: DELETE FROM CacheData WHERE key = ?; From b50969437df63935db744977d7c09bbf57093500 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2026 09:01:35 +0200 Subject: [PATCH 8/9] Ensure clearing the cache delete the CacheStore. --- features/preferences/impl/build.gradle.kts | 2 ++ .../impl/tasks/ClearCacheUseCase.kt | 4 ++++ .../impl/tasks/DefaultClearCacheUseCaseTest.kt | 7 +++++++ .../libraries/cachestore/api/CacheStore.kt | 1 + .../cachestore/impl/DatabaseCacheStore.kt | 8 ++++++-- .../android/libraries/cachestore/CacheData.sq | 3 +++ .../impl/DatabaseCacheStoreTest.kt | 18 ++++++++++++++++++ .../sessionstorage/test/InMemoryCacheStore.kt | 4 ++++ 8 files changed, 45 insertions(+), 2 deletions(-) diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index ad28c90966..33936760f5 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) + implementation(projects.libraries.cachestore.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) @@ -114,6 +115,7 @@ dependencies { testImplementation(projects.features.logout.test) testImplementation(projects.libraries.indicator.test) testImplementation(projects.libraries.pushproviders.test) + testImplementation(projects.libraries.cachestore.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.appnavstate.impl) testImplementation(projects.services.analytics.test) 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 index 6c26866e93..141adafe2b 100644 --- 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 @@ -14,6 +14,7 @@ import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Provider import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.preferences.impl.DefaultCacheService +import io.element.android.libraries.cachestore.api.CacheStore import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.ApplicationContext @@ -37,8 +38,11 @@ class DefaultClearCacheUseCase( private val pushService: PushService, private val seenInvitesStore: SeenInvitesStore, private val activeRoomsHolder: ActiveRoomsHolder, + private val cacheStore: CacheStore, ) : ClearCacheUseCase { override suspend fun invoke() = withContext(coroutineDispatchers.io) { + // Clear cache store + cacheStore.deleteAll() // Active rooms should be disposed of before clearing the cache activeRoomsHolder.clear(matrixClient.sessionId) // Clear Matrix cache diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt index 6845ecb3a4..1c1cb83def 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt @@ -19,6 +19,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.push.test.FakePushService +import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore +import io.element.android.libraries.sessionstorage.test.aCacheData import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -49,6 +51,9 @@ class DefaultClearCacheUseCaseTest { ) val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID)) assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty() + val cacheStore = InMemoryCacheStore( + initialData = mapOf("key1" to aCacheData()) + ) val sut = DefaultClearCacheUseCase( context = InstrumentationRegistry.getInstrumentation().context, matrixClient = matrixClient, @@ -58,9 +63,11 @@ class DefaultClearCacheUseCaseTest { pushService = pushService, seenInvitesStore = seenInvitesStore, activeRoomsHolder = activeRoomsHolder, + cacheStore = cacheStore, ) defaultCacheService.clearedCacheEventFlow.test { sut.invoke() + assertThat(cacheStore.dataMap).isEmpty() clearCacheLambda.assertions().isCalledOnce() setIgnoreRegistrationErrorLambda.assertions().isCalledOnce() .with(value(matrixClient.sessionId), value(false)) diff --git a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt index f16a663ed7..5df446f688 100644 --- a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt +++ b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt @@ -11,4 +11,5 @@ interface CacheStore { suspend fun storeData(key: String, data: CacheData) suspend fun getData(key: String): CacheData? suspend fun deleteData(key: String) + suspend fun deleteAll() } diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt index 126387da11..54766803f9 100644 --- a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt @@ -27,10 +27,14 @@ class DatabaseCacheStore( override suspend fun storeData(key: String, data: CacheData) { database.cacheDataQueries.insertData( data.toDbModel(key) - ) + ).await() } override suspend fun deleteData(key: String) { - database.cacheDataQueries.deleteData(key) + database.cacheDataQueries.deleteData(key).await() + } + + override suspend fun deleteAll() { + database.cacheDataQueries.deleteAll().await() } } diff --git a/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq index aa8bacf90b..fd350ac7ba 100644 --- a/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq +++ b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq @@ -23,3 +23,6 @@ INSERT OR REPLACE INTO CacheData VALUES ?; deleteData: DELETE FROM CacheData WHERE key = ?; + +deleteAll: +DELETE FROM CacheData; diff --git a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt index a2216daa06..36d7e05532 100644 --- a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt +++ b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt @@ -65,4 +65,22 @@ class DatabaseCacheStoreTest { databaseCacheStore.deleteData(A_KEY) assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() } + + @Test + fun `deleteAll deletes all the data`() = runTest { + // Assert that no data is stored for the key + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + // Store data + databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_1, 1)) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo( + DbCacheData( + key = A_KEY, + value_ = A_DATA_1, + updatedAt = 1, + ) + ) + // Delete all data + databaseCacheStore.deleteAll() + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + } } diff --git a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt index b0f77062ba..f029c8bcde 100644 --- a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt +++ b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt @@ -26,4 +26,8 @@ class InMemoryCacheStore( override suspend fun deleteData(key: String) { dataMap.remove(key) } + + override suspend fun deleteAll() { + dataMap.clear() + } } From a77c2c0c7e49203c22a6045361d65b46a1965982 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2026 17:54:22 +0200 Subject: [PATCH 9/9] Update ref to submodule. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 1ed9a7e8b4..fb7e9287d9 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 1ed9a7e8b4406a5e6d12f603cfc2083e84f8576f +Subproject commit fb7e9287d9d446012925139842d9aaa8e99a74dc