This commit is contained in:
bxdxnn
2026-08-24 10:30:25 +00:00
318 changed files with 5976 additions and 2161 deletions
@@ -8,9 +8,19 @@
package io.element.android.libraries.matrix.api
/**
* An interface to fetch content from a URL using a simple GET request.
* An interface to fetch content from a URL using a simple GET request launched from a Matrix client.
*/
fun interface UrlContentFetcher {
interface ClientUrlContentFetcher {
/**
* The domain of the public server for this homeserver.
*/
val server: String?
/**
* The URL of the actual Matrix homeserver.
*/
val homeserverUrl: String
/**
* Execute generic GET requests through the SDKs internal HTTP client.
*/
@@ -60,16 +60,13 @@ import java.util.Optional
* One instance exists per session, created on login or session restore and torn down by [logout], [clearCache] or account deactivation.
* Suspending members here report failures through a [Result] instead of throwing, and run their work off the main thread.
*/
interface MatrixClient : UrlContentFetcher {
interface MatrixClient : ClientUrlContentFetcher {
/** Identifier of the logged in user; a [SessionId] and a [UserId] are the same value. */
val sessionId: SessionId
/** Identifier of this device, as assigned by the homeserver when the session was created. */
val deviceId: DeviceId
/** Base URL of the homeserver this client is connected to. */
val homeserverUrl: String
/** Directories holding this session's databases, caches and downloaded media. */
val sessionPaths: SessionPaths
@@ -136,6 +133,9 @@ interface MatrixClient : UrlContentFetcher {
/** Scans media content before it is displayed, or `null` when no content scanner is configured for this session. */
val contentScanner: ContentScanner?
/** Whether the session is in the process of shutting down, either because it is being logged out or its cache is being cleared. */
val isShuttingDown: Boolean
/**
* Returns the room with the given id if the user has joined it, along with its live [Timeline].
* A new instance is created on each call and the caller owns it, so it must be closed once no longer needed.
@@ -245,6 +245,9 @@ interface MatrixClient : UrlContentFetcher {
/** Whether the homeserver advertises support for user status (MSC4426). */
suspend fun isUserStatusSupported(): Result<Boolean>
/** Whether the homeserver advertises support for the Profiles sliding sync extension (MSC4262). */
suspend fun isProfilesSlidingSyncExtensionSupported(): Result<Boolean>
/**
* Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call.
*
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api
/**
* An abstraction over the Matrix SDK's `ClientBuilder` to allow sharing it with other modules without exposing the SDK directly.
*/
interface MatrixClientBuilder
@@ -15,4 +15,4 @@ import java.lang.AutoCloseable
* Note: this client implements an [AutoCloseable] interface, so using [AutoCloseable.close] or [AutoCloseable.use] will clean up any resources associated
* with it, such as temporary files.
*/
interface TemporaryMatrixClient : UrlContentFetcher, AutoCloseable
interface TemporaryMatrixClient : ClientUrlContentFetcher, AutoCloseable
@@ -12,10 +12,10 @@ package io.element.android.libraries.matrix.api
*/
interface TemporaryMatrixClientFactory {
/**
* Builds an unauthenticated client pointing at [homeServerUrl], backed by a fresh set of temporary session directories.
* Builds an unauthenticated client pointing at [serverName], backed by a fresh set of temporary session directories.
* The caller owns the returned client and must close it to delete those directories.
*
* @param homeServerUrl the URL of the homeserver the client will talk to.
* @param serverName the domain of the public-facing server for the actual homeserver
*/
suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient>
suspend fun create(serverName: String): Result<TemporaryMatrixClient>
}
@@ -1,31 +0,0 @@
/*
* 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.api.scanner
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
/**
* Provides the URL of the content scanner service for a given homeserver, if any is set up.
*/
fun interface ContentScannerUrlProvider {
/**
* Returns the URL of the content scanner service for the given [sessionId], or `null` if no content scanner is set up.
*/
suspend fun getContentScannerUrl(sessionId: SessionId): Result<String?>
/**
* Creates a provider, which needs a fetcher because the scanner URL is itself discovered over HTTP.
*/
fun interface Factory {
/**
* @param urlContentFetcher used to read the configuration advertised by the homeserver; a client that is not authenticated yet also works.
*/
fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider
}
}
@@ -128,4 +128,11 @@ sealed interface GalleryItemType {
data class OtherMessageType(
val msgType: String,
val body: String,
) : MessageType
) : MessageType {
/**
* A `m.key.verification.request` carries a body meant for clients that cannot handle in-chat
* verification, so it must not be rendered as a message.
*/
val isKeyVerificationRequest: Boolean
get() = msgType == "m.key.verification.request"
}
@@ -135,6 +135,7 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
import java.util.Optional
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -145,7 +146,7 @@ import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
@Suppress("LargeClass")
class RustMatrixClient(
override val sessionPaths: SessionPaths,
private val innerClient: Client,
val innerClient: Client,
private val sessionStore: SessionStore,
private val sessionDelegate: RustClientSessionDelegate,
private val innerSyncService: ClientSyncService,
@@ -162,6 +163,7 @@ class RustMatrixClient(
) : MatrixClient {
override val sessionId: UserId = UserId(innerClient.userId())
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
override val server: String? = innerClient.server()
override val homeserverUrl: String = innerClient.homeserver()
override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId")
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
@@ -304,6 +306,10 @@ class RustMatrixClient(
.buffer(Channel.UNLIMITED)
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
private val _isShuttingDown = AtomicBoolean(false)
override val isShuttingDown: Boolean
get() = _isShuttingDown.get()
init {
// Make sure the session delegate has a reference to the client to be able to logout on auth error
sessionDelegate.bindClient(this)
@@ -320,7 +326,9 @@ class RustMatrixClient(
}
private suspend fun setupUserProfile() {
val supported = isUserStatusSupported().getOrDefault(false)
// Subscribing to own profile updates only requires the Profiles sliding sync extension,
// not the full user status capability (which also needs the status profile field to be settable).
val supported = isProfilesSlidingSyncExtensionSupported().getOrDefault(false)
if (supported) {
// No need to seed the data here, it's already stored by the sdk.
ownProfileTaskHandle = innerClient.subscribeToOwnProfile(ownProfileListener)
@@ -530,6 +538,12 @@ class RustMatrixClient(
}
}
override suspend fun isProfilesSlidingSyncExtensionSupported(): Result<Boolean> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.isProfilesSlidingSyncExtensionSupported()
}
}
override fun enableAutomaticCallStatus(enabled: Boolean) {
innerClient.enableAutomaticCallStatus(enabled)
}
@@ -666,11 +680,13 @@ class RustMatrixClient(
}
override suspend fun clearCache() {
_isShuttingDown.set(true)
innerClient.clearCaches(innerSyncService)
destroy()
}
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) {
_isShuttingDown.set(true)
sessionCoroutineScope.cancel()
// Remove current delegate so we don't receive an auth error
clientDelegateTaskHandle?.cancelAndDestroy()
@@ -708,6 +724,8 @@ class RustMatrixClient(
}
override suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit> = withContext(sessionDispatcher) {
_isShuttingDown.set(true)
Timber.w("Deactivating account")
// Remove current delegate so we don't receive an auth error
clientDelegateTaskHandle?.cancelAndDestroy()
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl
import io.element.android.libraries.matrix.api.MatrixClientBuilder
import org.matrix.rustcomponents.sdk.ClientBuilder
/**
* A wrapper around the Matrix SDK's [ClientBuilder] to allow sharing it with other modules without exposing the SDK directly.
*/
class RustMatrixClientBuilder(
val inner: ClientBuilder,
) : MatrixClientBuilder
@@ -9,6 +9,7 @@
package io.element.android.libraries.matrix.impl
import dev.zacsweers.metro.Inject
import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook
import io.element.android.libraries.androidutils.crypto.ClientSecret
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.ByteUnit
@@ -19,7 +20,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
@@ -37,7 +37,6 @@ 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.ContentScanner
import org.matrix.rustcomponents.sdk.CrossProcessLockConfig
import org.matrix.rustcomponents.sdk.RequestConfig
import org.matrix.rustcomponents.sdk.Session
@@ -70,7 +69,7 @@ class RustMatrixClientFactory(
private val clientBuilderProvider: ClientBuilderProvider,
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
private val workManagerScheduler: WorkManagerScheduler,
private val contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory,
private val clientBuilderEnterpriseHook: ClientBuilderEnterpriseHook,
) {
private val sessionDelegate = RustClientSessionDelegate(
sessionStore = sessionStore,
@@ -101,6 +100,7 @@ class RustMatrixClientFactory(
)
.homeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
.let { (clientBuilderEnterpriseHook(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner }
.use { it.build() }
client.setMediaRetentionPolicy(
@@ -135,21 +135,6 @@ class RustMatrixClientFactory(
client.setUtdDelegate(UtdTracker(analyticsService))
// If a content scanner URL is available for the homeserver, create a RustContentScanner and set it on the client.
// This allows the SDK to use the content scanner for automatic media scanning.
// If no content scanner URL is available, the contentScanner will be null.
val contentScannerUrlProvider = contentScannerUrlProviderFactory.create(RustTemporaryMatrixClient(client, null))
val contentScanner = contentScannerUrlProvider.getContentScannerUrl(SessionId(client.userId()))
.getOrNull()
?.let { contentScannerUrl ->
val contentScanner = ContentScanner(contentScannerUrl)
client.setContentScanner(contentScanner)
RustContentScanner(
client = client,
rustScanner = contentScanner,
)
}
val syncService = client.syncService()
.withSharePos(true)
.withOfflineMode()
@@ -170,7 +155,7 @@ class RustMatrixClientFactory(
featureFlagService = featureFlagService,
analyticsService = analyticsService,
workManagerScheduler = workManagerScheduler,
contentScanner = contentScanner,
contentScanner = client.contentScanner()?.let { RustContentScanner(client, it) },
isMessageSearchAvailable = isMessageSearchAvailable,
).also {
Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
@@ -22,6 +22,9 @@ class RustTemporaryMatrixClient(
client.getUrl(url)
}.mapFailure { it.mapClientException() }
override val server: String? = client.server()
override val homeserverUrl: String = client.homeserver()
override fun close() {
client.close()
paths?.let {
@@ -19,7 +19,7 @@ class RustTemporaryMatrixClientFactory(
private val sessionPathsFactory: SessionPathsFactory,
private val rustMatrixClientFactory: RustMatrixClientFactory,
) : TemporaryMatrixClientFactory {
override suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient> {
override suspend fun create(serverName: String): Result<TemporaryMatrixClient> {
return runCatchingExceptions {
val sessionPaths = sessionPathsFactory.create()
val client = rustMatrixClientFactory.getBaseClientBuilder(
@@ -28,7 +28,7 @@ class RustTemporaryMatrixClientFactory(
slidingSyncType = ClientBuilderSlidingSync.Native,
isMessageSearchAvailable = false,
)
.homeserverUrl(homeServerUrl)
.serverName(serverName)
.build()
RustTemporaryMatrixClient(client, sessionPaths)
}
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.auth
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.enterprise.api.ClientEnterpriseHook
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.crypto.ClientSecret
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -73,6 +74,7 @@ class RustMatrixAuthenticationService(
private val oAuthConfigurationProvider: OAuthConfigurationProvider,
private val enterpriseService: EnterpriseService,
private val featureFlagService: FeatureFlagService,
private val clientEnterpriseHook: ClientEnterpriseHook,
) : MatrixAuthenticationService {
// Any existing Element Classic session that we want to try to import secrets from during login.
private var elementClassicSession: ElementClassicSession? = null
@@ -166,6 +168,10 @@ class RustMatrixAuthenticationService(
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)
@@ -242,6 +248,9 @@ class RustMatrixAuthenticationService(
client.restoreSession(sessionData.toSession())
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
// Apply enterprise hooks to the newly created client as soon as possible
clientEnterpriseHook(matrixClient)
// We wait for the verification state to be known
matrixClient.waitForKnownVerificationState()
@@ -278,7 +287,6 @@ class RustMatrixAuthenticationService(
.let {
enterpriseService.tweakMasUrl(
url = it,
homeserver = client.server() ?: client.homeserver(),
urlContentFetcher = getUrlResolver,
)
}
@@ -333,6 +341,10 @@ class RustMatrixAuthenticationService(
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) }
@@ -398,6 +410,10 @@ class RustMatrixAuthenticationService(
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)
@@ -13,9 +13,9 @@ import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.scanner.ContentScanner
import io.element.android.libraries.matrix.api.search.MessageSearchService
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -111,7 +112,12 @@ object SessionMatrixModule {
}
@Provides
fun providesGetUrlResolverClient(matrixClient: MatrixClient): UrlContentFetcher {
fun providesGetUrlResolverClient(matrixClient: MatrixClient): ClientUrlContentFetcher {
return matrixClient
}
@Provides
fun providesMessageSearchService(matrixClient: MatrixClient): MessageSearchService {
return matrixClient.messageSearchService
}
}
@@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource
@@ -71,15 +71,8 @@ class RustMediaLoader(
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
filename = filename,
mimeType = when {
mimeType == null -> MimeTypes.OctetStream
MimeTypes.hasSubtype(mimeType) -> mimeType
// Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
mimeType == MimeTypes.Images -> MimeTypes.Jpeg
mimeType == MimeTypes.Videos -> MimeTypes.Mp4
mimeType == MimeTypes.Audio -> MimeTypes.Mp3
else -> MimeTypes.OctetStream
},
// Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
mimeType = mimeType.ensureDefaultSubtype(),
useCache = useCache,
tempDir = cacheDirectory.path,
)
@@ -11,7 +11,6 @@ package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
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
@@ -52,7 +51,6 @@ fun TestScope.createRustMatrixClientFactory(
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } },
) = RustMatrixClientFactory(
cacheDirectory = cacheDirectory,
appCoroutineScope = backgroundScope,
@@ -67,5 +65,5 @@ fun TestScope.createRustMatrixClientFactory(
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler,
contentScannerUrlProviderFactory = contentScannerUrlProviderFactory,
clientBuilderEnterpriseHook = { builder, _ -> builder },
)
@@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
@@ -64,7 +63,6 @@ class RustTemporaryMatrixClientFactoryTest {
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } },
) = RustMatrixClientFactory(
cacheDirectory = cacheDirectory,
appCoroutineScope = backgroundScope,
@@ -79,6 +77,6 @@ class RustTemporaryMatrixClientFactoryTest {
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler,
contentScannerUrlProviderFactory = contentScannerUrlProviderFactory,
clientBuilderEnterpriseHook = { builder, _ -> builder },
)
}
@@ -16,7 +16,6 @@ import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.NoHandle
import java.io.File
import kotlin.io.path.createTempDirectory
import org.matrix.rustcomponents.sdk.ClientException as RustClientException
@@ -84,7 +83,7 @@ class RustTemporaryMatrixClientTest {
}
private fun createRustTemporaryMatrixClient(
client: Client = Client(NoHandle),
client: Client = FakeFfiClient(),
paths: SessionPaths,
): RustTemporaryMatrixClient {
return RustTemporaryMatrixClient(client, paths)
@@ -98,6 +98,7 @@ class RustMatrixAuthenticationServiceTest {
),
enterpriseService = enterpriseService,
featureFlagService = FakeFeatureFlagService(),
clientEnterpriseHook = {},
)
}
}
@@ -10,13 +10,16 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
import io.element.android.libraries.matrix.impl.scanner.FakeFfiContentScanner
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_SERVER_NAME
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ContentScanner
import org.matrix.rustcomponents.sdk.CreateRoomParameters
import org.matrix.rustcomponents.sdk.Encryption
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
@@ -45,6 +48,7 @@ class FakeFfiClient(
private val userId: String = A_USER_ID.value,
private val deviceId: String = A_DEVICE_ID.value,
private val homeserver: String = A_HOMESERVER_URL,
private val server: String? = A_SERVER_NAME,
private val notificationClient: NotificationClient = FakeFfiNotificationClient(),
private val notificationSettings: NotificationSettings = FakeFfiNotificationSettings(),
private val encryption: Encryption = FakeFfiEncryption(),
@@ -59,13 +63,16 @@ class FakeFfiClient(
private val accountDataResult: (String) -> String? = { lambdaError() },
private val setAccountDataResult: (String, String) -> Unit = { _, _ -> lambdaError() },
private val isUserStatusSupportedResult: () -> Boolean = { false },
private val isProfilesSlidingSyncExtensionSupportedResult: () -> Boolean = { false },
private val subscribeToOwnProfileResult: (ProfileListener) -> Unit = {},
private val getUrlResult: (String) -> ByteArray = { lambdaError() },
private val contentScannerResult: () -> ContentScanner = { FakeFfiContentScanner() },
private val closeResult: () -> Unit = {},
) : Client(NoHandle) {
override fun userId(): String = userId
override fun deviceId(): String = deviceId
override fun homeserver(): String = homeserver
override fun server(): String? = server
override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient
override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings
override fun encryption(): Encryption = encryption
@@ -100,6 +107,8 @@ class FakeFfiClient(
override suspend fun isUserStatusSupported(): Boolean = isUserStatusSupportedResult()
override suspend fun isProfilesSlidingSyncExtensionSupported(): Boolean = isProfilesSlidingSyncExtensionSupportedResult()
override fun subscribeToOwnProfile(listener: ProfileListener): TaskHandle {
subscribeToOwnProfileResult(listener)
return FakeFfiTaskHandle()
@@ -139,5 +148,9 @@ class FakeFfiClient(
getUrlResult(url)
}
override suspend fun contentScanner(): ContentScanner = simulateLongTask {
contentScannerResult()
}
override fun close() = closeResult()
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.test
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
import io.element.android.tests.testutils.lambda.lambdaError
class FakeClientUrlContentFetcher(
override val server: String = A_SERVER_NAME,
override val homeserverUrl: String = A_HOMESERVER_URL,
private val getUrlResult: (String) -> Result<ByteArray> = { lambdaError() },
) : ClientUrlContentFetcher {
override suspend fun getUrl(url: String): Result<ByteArray> {
return getUrlResult(url)
}
}
@@ -79,6 +79,7 @@ class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID,
override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")),
override val deviceId: DeviceId = A_DEVICE_ID,
override val server: String = A_SERVER_NAME,
override val homeserverUrl: String = A_HOMESERVER_URL,
override val sessionCoroutineScope: CoroutineScope = TestScope(),
private val userDisplayName: String? = A_USER_NAME,
@@ -99,7 +100,7 @@ class FakeMatrixClient(
override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(),
private val accountManagementUrlResult: (AccountManagementAction?) -> Result<String?> = { lambdaError() },
private val resolveRoomAliasResult: (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
private val resolveRoomAliasResult: suspend (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
Result.success(
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
)
@@ -133,6 +134,7 @@ class FakeMatrixClient(
private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() },
private val enableAutomaticCallStatusLambda: (Boolean) -> Unit = { },
override val contentScanner: ContentScanner? = null,
private val isShuttingDownResult: () -> Boolean = { false },
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
@@ -147,6 +149,7 @@ class FakeMatrixClient(
var setUserStatusResult: Result<Unit> = Result.success(Unit)
var clearUserStatusResult: Result<Unit> = Result.success(Unit)
var isUserStatusSupportedResult: Result<Boolean> = Result.success(true)
var isProfilesSlidingSyncExtensionSupportedResult: Result<Boolean> = Result.success(true)
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl))
override val userProfile: StateFlow<MatrixUser> = _userProfile
@@ -293,6 +296,8 @@ class FakeMatrixClient(
override suspend fun isUserStatusSupported(): Result<Boolean> = isUserStatusSupportedResult
override suspend fun isProfilesSlidingSyncExtensionSupported(): Result<Boolean> = isProfilesSlidingSyncExtensionSupportedResult
override fun enableAutomaticCallStatus(enabled: Boolean) = enableAutomaticCallStatusLambda(enabled)
override suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?> = joinRoomLambda(roomId)
@@ -305,6 +310,9 @@ class FakeMatrixClient(
return knockRoomLambda(roomIdOrAlias, message, serverNames)
}
override val isShuttingDown: Boolean
get() = isShuttingDownResult()
// Mocks
fun givenCreateRoomResult(result: Result<RoomId>) {
@@ -11,10 +11,11 @@ import io.element.android.libraries.matrix.api.TemporaryMatrixClient
import io.element.android.tests.testutils.lambda.lambdaError
class FakeTemporaryMatrixClient(
override val server: String = A_SERVER_NAME,
override val homeserverUrl: String = A_HOMESERVER_URL,
private val getUrlResult: (String) -> Result<ByteArray> = { lambdaError() },
private val closeLambda: () -> Unit = {},
) : TemporaryMatrixClient {
override suspend fun getUrl(url: String): Result<ByteArray> = getUrlResult(url)
override fun close() = closeLambda()
}
@@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
class FakeTemporaryMatrixClientFactory(
private val createResult: (String) -> Result<TemporaryMatrixClient> = { Result.success(FakeTemporaryMatrixClient()) },
) : TemporaryMatrixClientFactory {
override suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient> = createResult(homeServerUrl)
override suspend fun create(serverName: String): Result<TemporaryMatrixClient> = createResult(serverName)
}
@@ -72,6 +72,7 @@ const val A_SPACE_NAME = "A space name"
const val A_REDACTION_REASON = "A redaction reason"
const val A_SERVER_NAME = "matrix.org"
const val A_HOMESERVER_URL = "matrix.org"
const val A_HOMESERVER_URL_2 = "matrix-client.org"
@@ -70,10 +70,15 @@ class FakeMatrixAuthenticationService(
return importCreatedSessionLambda(externalSession)
}
/** The login hint passed to the most recent [getOAuthUrl] call. */
var getOAuthUrlLoginHint: String? = null
private set
override suspend fun getOAuthUrl(
prompt: OAuthPrompt,
loginHint: String?,
): Result<OAuthDetails> = simulateLongTask {
getOAuthUrlLoginHint = loginHint
oAuthError?.let { Result.failure(it) } ?: Result.success(AN_OAUTH_DATA)
}