Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion enterprise
Original file line number Diff line number Diff line change
Expand Up @@ -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 beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder

/**
* Customize the [MatrixClientBuilder] for enterprise features.
* 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 beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder {
// No modification
return clientBuilder
}

override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
// No modification
return clientBuilder
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 beforeClientCreationResult: (MatrixClientBuilder) -> MatrixClientBuilder = { it },
private val beforeClientCreationWithSessionResult: (MatrixClientBuilder, SessionId) -> MatrixClientBuilder = { clientBuilder, _ -> clientBuilder },
) : ClientBuilderEnterpriseHook {
override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder {
return beforeClientCreationResult(clientBuilder)
}

override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
return beforeClientCreationWithSessionResult(clientBuilder, sessionId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,7 +99,12 @@ class RustMatrixClientFactory(
)
.homeserverUrl(sessionData.homeserverUrl)
.enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination))
.let { (clientBuilderEnterpriseHook(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner }
.let {
(clientBuilderEnterpriseHook.beforeClientCreationWithSession(
clientBuilder = RustMatrixClientBuilder(it),
sessionId = SessionId(sessionData.userId),
) as RustMatrixClientBuilder).inner
}
.use { it.build() }

client.setMediaRetentionPolicy(
Expand All @@ -118,14 +122,6 @@ class RustMatrixClientFactory(

client.restoreSession(sessionData.toSession())

create(client, sessionData, isMessageSearchAvailable)
}

suspend fun create(
client: Client,
sessionData: SessionData,
isMessageSearchAvailable: Boolean,
): RustMatrixClient {
Comment on lines -124 to -128

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note by doing this we're not reusing an already valid client and are forcing the creation of a different client, which can mean ~1s for opening the SDK DBs again I think. It's not that important since it happens on the login flow, but that's why we had 2 methods and tried to avoid destroying the current client in the log in flows.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. So maybe we could have a second hook to update the client once it's logged in? Not sure if this will work for @richvdh .

To let this PR focused on this change, I'll extract the first commit to its own PR.

@bmarty bmarty Aug 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have measured 580ms spent on the new method finalizeClientCreation, but only 80 ms for the method restoreSession in it, so the impact is actually very small (?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory we could work around it, but for my usecase it would be much better to run the hook before the client is created. I don't need to know the userid though, so provided the hook actually gets called whenever a client is created, that would be fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand, how the hook using MatrixClient can be called before MatrixClient is created? Are you talking about ClientEnterpriseHook or something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm talking about ClientBuilderEnterpriseHook. I need a way to call methods on the underlying rust-side ClientBuilder each time we build a Client.

I could probably work around it by adding methods to the rust-side MatrixClient and calling them after the client is created, but I'd much prefer to avoid that.

val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()

client.setUtdDelegate(UtdTracker(analyticsService))
Expand All @@ -136,7 +132,7 @@ class RustMatrixClientFactory(
.withProfilesExtension()
.finish()

return RustMatrixClient(
RustMatrixClient(
sessionPaths = sessionData.getSessionPaths(),
innerClient = client,
sessionStore = sessionStore,
Expand Down Expand Up @@ -228,6 +224,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.beforeClientCreation(RustMatrixClientBuilder(it)) as RustMatrixClientBuilder).inner
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that matrixClient.waitForKnownVerificationState() was not called in this case.


// 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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -357,6 +337,7 @@ class RustMatrixAuthenticationService(
sessionPaths = emptySessionPaths,
qrCodeData = sdkQrCodeLoginData,
)
currentClient = client
client.newLoginWithQrCodeHandler(
oauthConfiguration = oAuthConfiguration,
).use {
Expand All @@ -374,32 +355,44 @@ 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)
is HumanQrLoginException -> QrErrorMapper.map(it)
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
}
Timber.e(throwable, "Failed to login with QR code")
}
}

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)

matrixClient.waitForKnownVerificationState()

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)
}

private suspend fun makeClient(
sessionPaths: SessionPaths,
config: suspend ClientBuilder.() -> ClientBuilder,
Expand Down Expand Up @@ -449,10 +442,8 @@ class RustMatrixAuthenticationService(
.build()
}

private fun clear(destroyClient: Boolean) {
if (destroyClient) {
currentClient?.close()
}
private fun clear() {
currentClient?.close()
currentClient = null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
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
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
Expand Down Expand Up @@ -51,6 +53,7 @@ fun TestScope.createRustMatrixClientFactory(
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
) = RustMatrixClientFactory(
cacheDirectory = cacheDirectory,
appCoroutineScope = backgroundScope,
Expand All @@ -63,7 +66,7 @@ fun TestScope.createRustMatrixClientFactory(
featureFlagService = FakeFeatureFlagService(),
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
sqliteStoreBuilderProvider = sqliteStoreBuilderProvider,
workManagerScheduler = workManagerScheduler,
clientBuilderEnterpriseHook = { builder, _ -> builder },
clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,6 +78,6 @@ class RustTemporaryMatrixClientFactoryTest {
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler,
clientBuilderEnterpriseHook = { builder, _ -> builder },
clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(),
)
}
Loading
Loading