From 5ce531fd44905431bf3e2d541eccf9672e767bc7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 28 Aug 2026 17:30:37 +0200 Subject: [PATCH 1/7] Ensure we create a fresh Client when the sessionId is know after authentication. --- .../matrix/impl/RustMatrixClientFactory.kt | 11 +-- .../auth/RustMatrixAuthenticationService.kt | 91 ++++++++----------- 2 files changed, 38 insertions(+), 64 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index de29e7a9537..db391b3fdcf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -35,7 +35,6 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.CrossProcessLockConfig import org.matrix.rustcomponents.sdk.RequestConfig @@ -118,14 +117,6 @@ class RustMatrixClientFactory( client.restoreSession(sessionData.toSession()) - create(client, sessionData, isMessageSearchAvailable) - } - - suspend fun create( - client: Client, - sessionData: SessionData, - isMessageSearchAvailable: Boolean, - ): RustMatrixClient { val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens() client.setUtdDelegate(UtdTracker(analyticsService)) @@ -136,7 +127,7 @@ class RustMatrixClientFactory( .withProfilesExtension() .finish() - return RustMatrixClient( + RustMatrixClient( sessionPaths = sessionData.getSessionPaths(), innerClient = client, sessionStore = sessionStore, 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 a175f253ca4..e0758b590df 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 @@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.impl.keys.SecretGenerator import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory import io.element.android.libraries.sessionstorage.api.LoginType +import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.first @@ -101,17 +102,7 @@ class RustMatrixAuthenticationService( runCatchingExceptions { val sessionData = sessionStore.getSession(sessionId.value) if (sessionData != null) { - if (sessionData.isTokenValid) { - // Use the sessionData.passphrase, which can be null for a previously created session - if (sessionData.passphrase == null) { - Timber.w("Restoring a session without a passphrase") - } else { - Timber.w("Restoring a session with a passphrase") - } - rustMatrixClientFactory.create(sessionData) - } else { - throw SessionRestorationException.InvalidToken() - } + restoreSession(sessionData) } else { throw SessionRestorationException.MissingSession(sessionId) } @@ -120,6 +111,19 @@ class RustMatrixAuthenticationService( } } + private suspend fun restoreSession(sessionData: SessionData): MatrixClient { + if (!sessionData.isTokenValid) { + throw SessionRestorationException.InvalidToken() + } + // Use the sessionData.passphrase, which can be null for a previously created session + if (sessionData.passphrase == null) { + Timber.w("Restoring a session without a passphrase") + } else { + Timber.w("Restoring a session with a passphrase") + } + return rustMatrixClientFactory.create(sessionData) + } + private fun getDatabaseKey(): ClientSecret { Timber.d("New sessions will be encrypted with a raw key") return secretGenerator.generateKey() @@ -137,7 +141,7 @@ class RustMatrixAuthenticationService( client.homeserverLoginDetails().map() }.onFailure { - clear(destroyClient = true) + clear() }.mapFailure { failure -> Timber.e(failure, "Failed to set homeserver to $homeserver") failure.mapAuthenticationException() @@ -165,18 +169,7 @@ class RustMatrixAuthenticationService( passphrase = pendingKey.formattedAsString(), sessionPaths = currentSessionPaths, ) - val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) - - // Apply enterprise hooks to the newly created client as soon as possible - clientEnterpriseHook(matrixClient) - - newMatrixClientObservers.forEach { it.invoke(matrixClient) } - sessionStore.addSession(sessionData) - - // Clean up the strong reference held here since it's no longer necessary - clear(destroyClient = false) - - SessionId(sessionData.userId) + finalizeClientCreation(sessionData) }.mapFailure { failure -> Timber.e(failure, "Failed to login") failure.mapAuthenticationException() @@ -305,20 +298,7 @@ class RustMatrixAuthenticationService( passphrase = pendingKey.formattedAsString(), sessionPaths = currentSessionPaths, ) - val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) - - // Apply enterprise hooks to the newly created client as soon as possible - clientEnterpriseHook(matrixClient) - - matrixClient.waitForKnownVerificationState() - - newMatrixClientObservers.forEach { it.invoke(matrixClient) } - sessionStore.addSession(sessionData) - - // Clean up the strong reference held here since it's no longer necessary - clear(destroyClient = false) - - SessionId(sessionData.userId) + finalizeClientCreation(sessionData) }.mapFailure { failure -> Timber.e(failure, "Failed to login with OAuth") failure.mapAuthenticationException() @@ -374,18 +354,7 @@ class RustMatrixAuthenticationService( passphrase = pendingKey.formattedAsString(), sessionPaths = emptySessionPaths, ) - val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) - - // Apply enterprise hooks to the newly created client as soon as possible - clientEnterpriseHook(matrixClient) - - newMatrixClientObservers.forEach { it.invoke(matrixClient) } - sessionStore.addSession(sessionData) - - // Clean up the strong reference held here since it's no longer necessary - clear(destroyClient = false) - - SessionId(sessionData.userId) + finalizeClientCreation(sessionData) }.mapFailure { when (it) { is QrCodeDecodeException -> QrErrorMapper.map(it) @@ -400,6 +369,22 @@ class RustMatrixAuthenticationService( } } + private suspend fun finalizeClientCreation(sessionData: SessionData): SessionId { + val matrixClient = restoreSession(sessionData) + // Apply enterprise hooks to the newly created client as soon as possible + clientEnterpriseHook(matrixClient) + + matrixClient.waitForKnownVerificationState() + + newMatrixClientObservers.forEach { it.invoke(matrixClient) } + sessionStore.addSession(sessionData) + + // Clean up the strong reference held here since it's no longer necessary + clear() + + return SessionId(sessionData.userId) + } + private suspend fun makeClient( sessionPaths: SessionPaths, config: suspend ClientBuilder.() -> ClientBuilder, @@ -449,10 +434,8 @@ class RustMatrixAuthenticationService( .build() } - private fun clear(destroyClient: Boolean) { - if (destroyClient) { - currentClient?.close() - } + private fun clear() { + currentClient?.close() currentClient = null } From 1f18a18917763218e809d7e54ff66ee4b36823ca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2026 17:08:10 +0200 Subject: [PATCH 2/7] Iterate on hooks implementation --- enterprise | 2 +- .../api/ClientBuilderEnterpriseHook.kt | 21 ++++++++++++++-- .../DefaultClientBuilderEnterpriseHook.kt | 7 +++++- .../test/FakeClientBuilderEnterpriseHook.kt | 25 +++++++++++++++++++ .../matrix/impl/RustMatrixClientFactory.kt | 7 +++++- .../impl/RustMatrixClientFactoryTest.kt | 3 ++- .../RustTemporaryMatrixClientFactoryTest.kt | 3 ++- 7 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt diff --git a/enterprise b/enterprise index bb07b4f8391..10b18f14169 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit bb07b4f839163034fab9c3d1b4ae364f0839b450 +Subproject commit 10b18f141697b8a3998022b1cb7ff9f4a9f1171c diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt index bfb9fb538a4..f60b647c5d2 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt @@ -13,6 +13,23 @@ import io.element.android.libraries.matrix.api.core.SessionId /** * A hook that can be used to customize the [MatrixClientBuilder] for enterprise features. */ -fun interface ClientBuilderEnterpriseHook { - suspend operator fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder +interface ClientBuilderEnterpriseHook { + /** + * Customize the [MatrixClientBuilder] for enterprise features. + * This method is invoked everytime a new [MatrixClientBuilder] is created. + * + * @param clientBuilder The [MatrixClientBuilder] to customize. + * @return The customized [MatrixClientBuilder]. + */ + suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder): MatrixClientBuilder + + /** + * Customize the [MatrixClientBuilder] for enterprise features. + * This method is invoked when a new [MatrixClientBuilder] is created to build a client for a specific session. + * + * @param clientBuilder The [MatrixClientBuilder] to customize. + * @param sessionId The [SessionId] for which the [MatrixClientBuilder] is being created. + * @return The customized [MatrixClientBuilder]. + */ + suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder } diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt index ee22e4399da..e4b6df41713 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt @@ -18,7 +18,12 @@ import io.element.android.libraries.matrix.api.core.SessionId */ @ContributesBinding(AppScope::class) class DefaultClientBuilderEnterpriseHook : ClientBuilderEnterpriseHook { - override suspend fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { + override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder): MatrixClientBuilder { + // No modification + return clientBuilder + } + + override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { // No modification return clientBuilder } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt new file mode 100644 index 00000000000..8a8c3c5a672 --- /dev/null +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt @@ -0,0 +1,25 @@ +/* + * 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.features.enterprise.test + +import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook +import io.element.android.libraries.matrix.api.MatrixClientBuilder +import io.element.android.libraries.matrix.api.core.SessionId + +class FakeClientBuilderEnterpriseHook( + private val tweakClientBuilderResult: (MatrixClientBuilder) -> MatrixClientBuilder = { it }, + private val tweakClientBuilderWithSessionIdResult: (MatrixClientBuilder, SessionId) -> MatrixClientBuilder = { clientBuilder, _ -> clientBuilder }, +) : ClientBuilderEnterpriseHook { + override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder): MatrixClientBuilder { + return tweakClientBuilderResult(clientBuilder) + } + + override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { + return tweakClientBuilderWithSessionIdResult(clientBuilder, sessionId) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index db391b3fdcf..8f07b3c546b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -99,7 +99,9 @@ class RustMatrixClientFactory( ) .homeserverUrl(sessionData.homeserverUrl) .enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination)) - .let { (clientBuilderEnterpriseHook(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner } + .let { + (clientBuilderEnterpriseHook.tweakClientBuilder(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner + } .use { it.build() } client.setMediaRetentionPolicy( @@ -219,6 +221,9 @@ class RustMatrixClientFactory( // Workaround for non-nullable proxy parameter in the SDK, since each call to the ClientBuilder returns a new reference we need to keep proxyProvider.provides()?.let { proxy(it) } ?: this } + .let { + (clientBuilderEnterpriseHook.tweakClientBuilder(RustMatrixClientBuilder(it)) as RustMatrixClientBuilder).inner + } } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index 6d3afad5c32..6b5b5a154a4 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeClientBuilderEnterpriseHook import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider @@ -65,5 +66,5 @@ fun TestScope.createRustMatrixClientFactory( clientBuilderProvider = clientBuilderProvider, sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), workManagerScheduler = workManagerScheduler, - clientBuilderEnterpriseHook = { builder, _ -> builder }, + clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(), ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt index d0b87ef53a0..f18f6338898 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeClientBuilderEnterpriseHook import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory @@ -77,6 +78,6 @@ class RustTemporaryMatrixClientFactoryTest { clientBuilderProvider = clientBuilderProvider, sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), workManagerScheduler = workManagerScheduler, - clientBuilderEnterpriseHook = { builder, _ -> builder }, + clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(), ) } From 579ec9bd6a732d46ef12e42121f3c1bb614f05d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2026 17:23:29 +0200 Subject: [PATCH 3/7] Close the client used to log in before creating the client of the session Since the login flows stopped handing their client over to RustMatrixClientFactory and let finalizeClientCreation() build a fresh one from the SessionData, two problems appeared. The client built by loginWithQrCode() was never owned by anyone: it is not assigned to currentClient, so clear() had nothing to close and the Rust Client stayed open for the whole app lifetime, holding the session stores of the freshly logged in account. It is now tracked in currentClient, and also disposed of when the QR login fails, which is safe because that flow always builds its own client. The client of the session was also built while the login client was still open. Both use the same session paths, so their state and crypto SQLite stores, and the search index when message search is enabled, were opened twice at the same time with no cross process lock to protect them. clear() now runs before the final client is created. Co-Authored-By: Claude Opus 5 (1M context) --- .../auth/RustMatrixAuthenticationService.kt | 10 +- .../RustMatrixAuthenticationServiceTest.kt | 106 +++++++++++++++++- .../impl/fixtures/fakes/FakeFfiClient.kt | 12 ++ .../fakes/FakeFfiLoginWithQrCodeHandler.kt | 37 ++++++ .../impl/fixtures/fakes/FakeFfiQrCodeData.kt | 5 + 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt 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 e0758b590df..771bc60a052 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 @@ -337,6 +337,7 @@ class RustMatrixAuthenticationService( sessionPaths = emptySessionPaths, qrCodeData = sdkQrCodeLoginData, ) + currentClient = client client.newLoginWithQrCodeHandler( oauthConfiguration = oAuthConfiguration, ).use { @@ -362,6 +363,8 @@ class RustMatrixAuthenticationService( else -> it } }.onFailure { throwable -> + // A QR code login always builds its own client, so it can be disposed of on failure. + clear() if (throwable is CancellationException) { throw throwable } @@ -370,6 +373,10 @@ class RustMatrixAuthenticationService( } private suspend fun finalizeClientCreation(sessionData: SessionData): SessionId { + // Close the client which was used to perform the login before creating the final client. + // Both use the same session paths, so their SQLite stores must never be opened at the same time. + clear() + val matrixClient = restoreSession(sessionData) // Apply enterprise hooks to the newly created client as soon as possible clientEnterpriseHook(matrixClient) @@ -379,9 +386,6 @@ class RustMatrixAuthenticationService( newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.addSession(sessionData) - // Clean up the strong reference held here since it's no longer necessary - clear() - return SessionId(sessionData.userId) } 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 5d66e4f4bd8..fb4fb80e1e3 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 @@ -14,20 +14,29 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.impl.ClientBuilderProvider import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider +import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiLoginWithQrCodeHandler +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.ClientBuilder +import org.matrix.rustcomponents.sdk.HumanQrLoginException import java.io.File class RustMatrixAuthenticationServiceTest { @@ -74,8 +83,102 @@ class RustMatrixAuthenticationServiceTest { closeResult.assertions().isCalledOnce() } + @Test + fun `login closes the client used to log in before building the client of the session`() = runTest { + val events = mutableListOf() + val sut = createRustMatrixAuthenticationService( + clientBuilderProvider = FakeSequentialClientBuilderProvider( + { + events.add("build login client") + FakeFfiClient( + homeserverLoginDetailsResult = { FakeFfiHomeserverLoginDetails() }, + loginResult = { _, _ -> }, + closeResult = { events.add("close login client") }, + ) + }, + { + events.add("build session client") + FakeFfiClient(withUtdHook = {}) + }, + ), + ) + + assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue() + assertThat(sut.login("alice", "password").getOrNull()).isEqualTo(A_USER_ID) + + // The two clients share the same session paths, so the login one must be closed first. + assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder() + } + + @Test + fun `loginWithQrCode closes the client used to log in before building the client of the session`() = runTest { + val events = mutableListOf() + val sut = createRustMatrixAuthenticationService( + clientBuilderProvider = FakeSequentialClientBuilderProvider( + { + events.add("build login client") + FakeFfiClient( + newLoginWithQrCodeHandlerResult = { FakeFfiLoginWithQrCodeHandler() }, + closeResult = { events.add("close login client") }, + ) + }, + { + events.add("build session client") + FakeFfiClient(withUtdHook = {}) + }, + ), + ) + + val result = sut.loginWithQrCode(aSdkQrCodeLoginData()) {} + + assertThat(result.getOrNull()).isEqualTo(A_USER_ID) + assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder() + } + + @Test + fun `loginWithQrCode closes the client it created when the login fails`() = runTest { + val closeResult = lambdaRecorder {} + val sut = createRustMatrixAuthenticationService( + clientBuilderProvider = FakeClientBuilderProvider( + provideResult = { + FakeFfiClientBuilder( + buildResult = { + FakeFfiClient( + newLoginWithQrCodeHandlerResult = { + FakeFfiLoginWithQrCodeHandler( + scanResult = { throw HumanQrLoginException.Unknown() }, + ) + }, + closeResult = closeResult, + ) + }, + ) + }, + ), + ) + + assertThat(sut.loginWithQrCode(aSdkQrCodeLoginData()) {}.isFailure).isTrue() + closeResult.assertions().isCalledOnce() + } + + private fun aSdkQrCodeLoginData() = SdkQrCodeLoginData( + FakeFfiQrCodeData( + baseUrlResult = { A_HOMESERVER_URL }, + ) + ) + + /** + * A [ClientBuilderProvider] handing out one [Client] per call, in order. + */ + private class FakeSequentialClientBuilderProvider( + private vararg val clients: () -> Client, + ) : ClientBuilderProvider { + private var index = 0 + override fun provide(): ClientBuilder = FakeFfiClientBuilder(buildResult = clients[index++]) + } + private fun TestScope.createRustMatrixAuthenticationService( - sessionStore: SessionStore = InMemorySessionStore(), + sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _ -> }), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), enterpriseService: EnterpriseService = FakeEnterpriseService(), ): RustMatrixAuthenticationService { @@ -85,6 +188,7 @@ class RustMatrixAuthenticationServiceTest { cacheDirectory = cacheDirectory, sessionStore = sessionStore, clientBuilderProvider = clientBuilderProvider, + workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}), ) return RustMatrixAuthenticationService( sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 86587da6c1a..8c6411197a8 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -25,10 +25,12 @@ import org.matrix.rustcomponents.sdk.Encryption import org.matrix.rustcomponents.sdk.HomeserverCapabilities import org.matrix.rustcomponents.sdk.HomeserverLoginDetails import org.matrix.rustcomponents.sdk.IgnoredUsersListener +import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler import org.matrix.rustcomponents.sdk.NoHandle import org.matrix.rustcomponents.sdk.NotificationClient import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.NotificationSettings +import org.matrix.rustcomponents.sdk.OAuthConfiguration import org.matrix.rustcomponents.sdk.ProfileListener import org.matrix.rustcomponents.sdk.PusherIdentifiers import org.matrix.rustcomponents.sdk.PusherKind @@ -57,6 +59,8 @@ class FakeFfiClient( private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() }, private val getProfileResult: (String) -> UserProfile = { aRustUserProfile() }, private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() }, + private val loginResult: (String, String) -> Unit = { _, _ -> lambdaError() }, + private val newLoginWithQrCodeHandlerResult: () -> LoginWithQrCodeHandler = { lambdaError() }, private val getStoreSizesResult: () -> StoreSizes = { lambdaError() }, private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() }, private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(), @@ -122,6 +126,14 @@ class FakeFfiClient( return homeserverLoginDetailsResult() } + override suspend fun login(username: String, password: String, initialDeviceName: String?, deviceId: String?) { + loginResult(username, password) + } + + override fun newLoginWithQrCodeHandler(oauthConfiguration: OAuthConfiguration): LoginWithQrCodeHandler { + return newLoginWithQrCodeHandlerResult() + } + override suspend fun setMediaRetentionPolicy(policy: MediaRetentionPolicy) {} override suspend fun getStoreSizes(): StoreSizes { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt new file mode 100644 index 00000000000..6e6b7506438 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt @@ -0,0 +1,37 @@ +/* + * 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.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.GeneratedQrLoginProgressListener +import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler +import org.matrix.rustcomponents.sdk.NoHandle +import org.matrix.rustcomponents.sdk.QrCodeData +import org.matrix.rustcomponents.sdk.QrLoginProgress +import org.matrix.rustcomponents.sdk.QrLoginProgressListener + +class FakeFfiLoginWithQrCodeHandler( + private val generateResult: suspend () -> Unit = {}, + private val scanResult: suspend (QrCodeData) -> Unit = {}, +) : LoginWithQrCodeHandler(NoHandle) { + private var scanProgressListener: QrLoginProgressListener? = null + + override suspend fun generate(progressListener: GeneratedQrLoginProgressListener) { + generateResult() + } + + override suspend fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener) { + scanProgressListener = progressListener + scanResult(qrCodeData) + } + + fun emitScanProgress(progress: QrLoginProgress) { + scanProgressListener?.onUpdate(progress) + } + + override fun close() = Unit +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt index 4070d1b2cda..926cd85d300 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt @@ -14,12 +14,17 @@ import org.matrix.rustcomponents.sdk.QrCodeData class FakeFfiQrCodeData( private val serverNameResult: () -> String? = { lambdaError() }, + private val baseUrlResult: () -> String? = { lambdaError() }, private val toBytesResult: () -> ByteArray = { lambdaError() }, ) : QrCodeData(NoHandle) { override fun serverName(): String? { return serverNameResult() } + override fun baseUrl(): String? { + return baseUrlResult() + } + override fun toBytes(): ByteArray { return toBytesResult() } From 59e72fc3ee3167b99cb02f7bed5726fda4c75fa1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2026 17:28:34 +0200 Subject: [PATCH 4/7] Stop deleting the session data of a logged in account on the next login sessionPaths keeps pointing at the directories of the account which has just been logged in, and it was never reset. Adding another account calls setHomeserver(), whose rotateSessionPath() starts by deleting the directories of the previous sessionPaths, so the file and cache directories of the first account were wiped. finalizeClientCreation() now forgets the session paths once the login succeeded. It is done there rather than in clear() on purpose: after a failed attempt the directories are orphaned and must still be deleted by the rotateSessionPath() of the next attempt. Co-Authored-By: Claude Opus 5 (1M context) --- .../auth/RustMatrixAuthenticationService.kt | 4 + .../impl/RustMatrixClientFactoryTest.kt | 4 +- .../RustMatrixAuthenticationServiceTest.kt | 86 ++++++++++++++++++- 3 files changed, 89 insertions(+), 5 deletions(-) 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 771bc60a052..681aa6ba64c 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 @@ -386,6 +386,10 @@ class RustMatrixAuthenticationService( newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.addSession(sessionData) + // The session paths now hold the data of the account which has just been logged in, so forget + // them: they must not be deleted by the rotateSessionPath() of the next login attempt. + sessionPaths = null + return SessionId(sessionData.userId) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index 6b5b5a154a4..b559b7d4d0f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider +import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider import io.element.android.libraries.network.useragent.SimpleUserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore @@ -52,6 +53,7 @@ fun TestScope.createRustMatrixClientFactory( ), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), + sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), ) = RustMatrixClientFactory( cacheDirectory = cacheDirectory, appCoroutineScope = backgroundScope, @@ -64,7 +66,7 @@ fun TestScope.createRustMatrixClientFactory( featureFlagService = FakeFeatureFlagService(), timelineEventFilterFactory = FakeTimelineEventFilterFactory(), clientBuilderProvider = clientBuilderProvider, - sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), + sqliteStoreBuilderProvider = sqliteStoreBuilderProvider, workManagerScheduler = workManagerScheduler, clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(), ) 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 fb4fb80e1e3..d81c6021dad 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 @@ -12,6 +12,7 @@ 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.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.matrix.impl.ClientBuilderProvider import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData @@ -22,6 +23,10 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserver import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiLoginWithQrCodeHandler import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory +import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilder +import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider +import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilder +import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider @@ -33,13 +38,18 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.HumanQrLoginException import java.io.File class RustMatrixAuthenticationServiceTest { + @get:Rule + val temporaryFolder = TemporaryFolder() + @Test fun `setHomeserver is successful`() = runTest { val sut = createRustMatrixAuthenticationService( @@ -177,21 +187,89 @@ class RustMatrixAuthenticationServiceTest { override fun provide(): ClientBuilder = FakeFfiClientBuilder(buildResult = clients[index++]) } + @Test + fun `a new login does not delete the session data of the account which has just been logged in`() = runTest { + val storeBuilderProvider = SessionDirectoryCreatingSqliteStoreBuilderProvider() + val sut = createRustMatrixAuthenticationService( + clientBuilderProvider = FakeSequentialClientBuilderProvider( + { aLoginFakeFfiClient() }, + { FakeFfiClient(withUtdHook = {}) }, + { aLoginFakeFfiClient() }, + ), + sessionPathsFactory = SessionPathsFactory(temporaryFolder.newFolder("base"), temporaryFolder.newFolder("cache")), + sqliteStoreBuilderProvider = storeBuilderProvider, + ) + + assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue() + assertThat(sut.login("alice", "password").getOrNull()).isEqualTo(A_USER_ID) + val loggedInSessionPaths = storeBuilderProvider.providedSessionPaths.first() + + // Adding another account rotates the session paths, which must not touch the previous account. + assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue() + + assertThat(loggedInSessionPaths.fileDirectory.exists()).isTrue() + assertThat(loggedInSessionPaths.cacheDirectory.exists()).isTrue() + } + + @Test + fun `a new login deletes the session data of a previous failed login attempt`() = runTest { + val storeBuilderProvider = SessionDirectoryCreatingSqliteStoreBuilderProvider() + val sut = createRustMatrixAuthenticationService( + clientBuilderProvider = FakeSequentialClientBuilderProvider( + { aLoginFakeFfiClient() }, + { throw IllegalStateException("Failed to build the client of the session") }, + { aLoginFakeFfiClient() }, + ), + sessionPathsFactory = SessionPathsFactory(temporaryFolder.newFolder("base"), temporaryFolder.newFolder("cache")), + sqliteStoreBuilderProvider = storeBuilderProvider, + ) + + assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue() + assertThat(sut.login("alice", "password").isFailure).isTrue() + val abandonedSessionPaths = storeBuilderProvider.providedSessionPaths.first() + + assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue() + + assertThat(abandonedSessionPaths.fileDirectory.exists()).isFalse() + assertThat(abandonedSessionPaths.cacheDirectory.exists()).isFalse() + } + + private fun aLoginFakeFfiClient() = FakeFfiClient( + homeserverLoginDetailsResult = { FakeFfiHomeserverLoginDetails() }, + loginResult = { _, _ -> }, + ) + + /** + * A [SqliteStoreBuilderProvider] which creates the session directories, like the SDK does when it + * opens its stores, and records them so that a test can assert on their lifecycle. + */ + private class SessionDirectoryCreatingSqliteStoreBuilderProvider : SqliteStoreBuilderProvider { + val providedSessionPaths = mutableListOf() + + override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder { + sessionPaths.fileDirectory.mkdirs() + sessionPaths.cacheDirectory.mkdirs() + providedSessionPaths.add(sessionPaths) + return FakeSqliteStoreBuilder() + } + } + private fun TestScope.createRustMatrixAuthenticationService( sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _ -> }), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), enterpriseService: EnterpriseService = FakeEnterpriseService(), + sessionPathsFactory: SessionPathsFactory = SessionPathsFactory(File("/base"), File("/cache")), + sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), ): RustMatrixAuthenticationService { - val baseDirectory = File("/base") - val cacheDirectory = File("/cache") val rustMatrixClientFactory = createRustMatrixClientFactory( - cacheDirectory = cacheDirectory, + cacheDirectory = File("/cache"), sessionStore = sessionStore, clientBuilderProvider = clientBuilderProvider, workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}), + sqliteStoreBuilderProvider = sqliteStoreBuilderProvider, ) return RustMatrixAuthenticationService( - sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory), + sessionPathsFactory = sessionPathsFactory, coroutineDispatchers = testCoroutineDispatchers(), sessionStore = sessionStore, rustMatrixClientFactory = rustMatrixClientFactory, From a817b6f219a1b9a3b8acd20065a858e4616a7ed4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Sep 2026 17:06:53 +0200 Subject: [PATCH 5/7] Rename methods and update documentation. --- enterprise | 2 +- .../enterprise/api/ClientBuilderEnterpriseHook.kt | 6 +++--- .../impl/DefaultClientBuilderEnterpriseHook.kt | 4 ++-- .../test/FakeClientBuilderEnterpriseHook.kt | 12 ++++++------ .../libraries/matrix/impl/RustMatrixClientFactory.kt | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/enterprise b/enterprise index 10b18f14169..ebe8b6a108d 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 10b18f141697b8a3998022b1cb7ff9f4a9f1171c +Subproject commit ebe8b6a108dcc823b7d123b94f00c11ed2d62d88 diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt index f60b647c5d2..883cab6840e 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt @@ -21,15 +21,15 @@ interface ClientBuilderEnterpriseHook { * @param clientBuilder The [MatrixClientBuilder] to customize. * @return The customized [MatrixClientBuilder]. */ - suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder): MatrixClientBuilder + suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder /** * Customize the [MatrixClientBuilder] for enterprise features. - * This method is invoked when a new [MatrixClientBuilder] is created to build a client for a specific session. + * This method is invoked as well as the other method when a new [MatrixClientBuilder] is created to build a client for a specific session. * * @param clientBuilder The [MatrixClientBuilder] to customize. * @param sessionId The [SessionId] for which the [MatrixClientBuilder] is being created. * @return The customized [MatrixClientBuilder]. */ - suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder + suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder } diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt index e4b6df41713..44887a07368 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt @@ -18,12 +18,12 @@ import io.element.android.libraries.matrix.api.core.SessionId */ @ContributesBinding(AppScope::class) class DefaultClientBuilderEnterpriseHook : ClientBuilderEnterpriseHook { - override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder): MatrixClientBuilder { + override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder { // No modification return clientBuilder } - override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { + override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { // No modification return clientBuilder } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt index 8a8c3c5a672..3781f253824 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt @@ -12,14 +12,14 @@ import io.element.android.libraries.matrix.api.MatrixClientBuilder import io.element.android.libraries.matrix.api.core.SessionId class FakeClientBuilderEnterpriseHook( - private val tweakClientBuilderResult: (MatrixClientBuilder) -> MatrixClientBuilder = { it }, - private val tweakClientBuilderWithSessionIdResult: (MatrixClientBuilder, SessionId) -> MatrixClientBuilder = { clientBuilder, _ -> clientBuilder }, + private val beforeClientCreationResult: (MatrixClientBuilder) -> MatrixClientBuilder = { it }, + private val beforeClientCreationWithSessionResult: (MatrixClientBuilder, SessionId) -> MatrixClientBuilder = { clientBuilder, _ -> clientBuilder }, ) : ClientBuilderEnterpriseHook { - override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder): MatrixClientBuilder { - return tweakClientBuilderResult(clientBuilder) + override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder { + return beforeClientCreationResult(clientBuilder) } - override suspend fun tweakClientBuilder(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { - return tweakClientBuilderWithSessionIdResult(clientBuilder, sessionId) + override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { + return beforeClientCreationWithSessionResult(clientBuilder, sessionId) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index 8f07b3c546b..16f33d91dbf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -100,7 +100,7 @@ class RustMatrixClientFactory( .homeserverUrl(sessionData.homeserverUrl) .enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination)) .let { - (clientBuilderEnterpriseHook.tweakClientBuilder(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner + (clientBuilderEnterpriseHook.beforeClientCreationWithSession(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner } .use { it.build() } @@ -222,7 +222,7 @@ class RustMatrixClientFactory( proxyProvider.provides()?.let { proxy(it) } ?: this } .let { - (clientBuilderEnterpriseHook.tweakClientBuilder(RustMatrixClientBuilder(it)) as RustMatrixClientBuilder).inner + (clientBuilderEnterpriseHook.beforeClientCreation(RustMatrixClientBuilder(it)) as RustMatrixClientBuilder).inner } } } From 3cde1523b4e51164cbeeb65ccffabecb80219a5f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Sep 2026 17:47:53 +0200 Subject: [PATCH 6/7] Fix quality and tests. --- .../android/libraries/matrix/impl/RustMatrixClientFactory.kt | 5 ++++- .../io/element/android/tests/konsist/KonsistClassNameTest.kt | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index 16f33d91dbf..b3113c9e055 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -100,7 +100,10 @@ class RustMatrixClientFactory( .homeserverUrl(sessionData.homeserverUrl) .enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination)) .let { - (clientBuilderEnterpriseHook.beforeClientCreationWithSession(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner + (clientBuilderEnterpriseHook.beforeClientCreationWithSession( + clientBuilder = RustMatrixClientBuilder(it), + sessionId = SessionId(sessionData.userId), + ) as RustMatrixClientBuilder).inner } .use { it.build() } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index 14a3f24f3c8..cb08d3b6303 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -105,6 +105,7 @@ class KonsistClassNameTest { "FakeListenableFuture", "FakeMediaStoreContentProvider", "FakeParentNode", + "FakeSequentialClientBuilderProvider", ) .assertTrue { val interfaceName = it.name From cbfef6a321e42cc83ef25312f2f705217621f40a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Sep 2026 12:35:45 +0200 Subject: [PATCH 7/7] Update ref --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index ebe8b6a108d..f463003f681 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit ebe8b6a108dcc823b7d123b94f00c11ed2d62d88 +Subproject commit f463003f6813ad33fc00f80786bc83b6a7f0d8e2