Merge pull request #7591 from element-hq/bma/iterateOnClientCreation

Iterate on client creation
This commit is contained in:
Benoit Marty
2026-09-11 13:22:42 +02:00
committed by GitHub
13 changed files with 355 additions and 77 deletions
@@ -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
@@ -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(
@@ -118,14 +122,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))
@@ -135,7 +131,7 @@ class RustMatrixClientFactory(
.withOfflineMode()
.finish()
return RustMatrixClient(
RustMatrixClient(
sessionPaths = sessionData.getSessionPaths(),
innerClient = client,
sessionStore = sessionStore,
@@ -227,6 +223,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
}
}
}
@@ -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()
@@ -357,6 +337,7 @@ class RustMatrixAuthenticationService(
sessionPaths = emptySessionPaths,
qrCodeData = sdkQrCodeLoginData,
)
currentClient = client
client.newLoginWithQrCodeHandler(
oauthConfiguration = oAuthConfiguration,
).use {
@@ -374,18 +355,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)
@@ -393,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
}
@@ -400,6 +372,27 @@ 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)
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,
@@ -449,10 +442,8 @@ class RustMatrixAuthenticationService(
.build()
}
private fun clear(destroyClient: Boolean) {
if (destroyClient) {
currentClient?.close()
}
private fun clear() {
currentClient?.close()
currentClient = null
}
@@ -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
@@ -51,6 +53,7 @@ fun TestScope.createRustMatrixClientFactory(
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
) = RustMatrixClientFactory(
cacheDirectory = cacheDirectory,
appCoroutineScope = backgroundScope,
@@ -63,7 +66,7 @@ fun TestScope.createRustMatrixClientFactory(
featureFlagService = FakeFeatureFlagService(),
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
sqliteStoreBuilderProvider = sqliteStoreBuilderProvider,
workManagerScheduler = workManagerScheduler,
clientBuilderEnterpriseHook = { builder, _ -> builder },
clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(),
)
@@ -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(),
)
}
@@ -12,25 +12,44 @@ 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
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.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
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.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(
@@ -74,20 +93,183 @@ 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<String>()
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<String>()
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<Unit> {}
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++])
}
@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<SessionPaths>()
override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder {
sessionPaths.fileDirectory.mkdirs()
sessionPaths.cacheDirectory.mkdirs()
providedSessionPaths.add(sessionPaths)
return FakeSqliteStoreBuilder()
}
}
private fun TestScope.createRustMatrixAuthenticationService(
sessionStore: SessionStore = InMemorySessionStore(),
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,
@@ -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 {
@@ -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
}
@@ -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()
}