Merge branch 'develop' into fix/5794-account-data-api

This commit is contained in:
Benoit Marty
2026-08-11 14:03:39 +02:00
committed by GitHub
650 changed files with 4781 additions and 3197 deletions
@@ -49,7 +49,6 @@ import io.element.android.libraries.matrix.api.scanner.ContentScanner
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.UserStatus
@@ -58,7 +57,6 @@ import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkDesktopHandler
import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkMobileHandler
import io.element.android.libraries.matrix.impl.linknewdevice.RustQrCodeDataParser
import io.element.android.libraries.matrix.impl.mapper.map
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
@@ -85,7 +83,9 @@ import io.element.android.libraries.matrix.impl.search.RustMessageSearchService
import io.element.android.libraries.matrix.impl.spaces.RustSpaceService
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.sync.map
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.user.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.user.into
import io.element.android.libraries.matrix.impl.user.map
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
@@ -126,9 +126,11 @@ import org.matrix.rustcomponents.sdk.IgnoredUsersListener
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.ProfileListener
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.UserProfile
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
@@ -271,8 +273,6 @@ class RustMatrixClient(
private var clientDelegateTaskHandle: TaskHandle? = innerClient.setDelegate(sessionDelegate)
@Volatile private var localUserStatus: UserStatus? = null
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(
MatrixUser(
userId = sessionId,
@@ -283,6 +283,14 @@ class RustMatrixClient(
override val userProfile: StateFlow<MatrixUser> = _userProfile
private var ownProfileTaskHandle: TaskHandle? = null
private val ownProfileListener = object : ProfileListener {
override fun onUpdate(profile: UserProfile) {
_userProfile.tryEmit(profile.map())
}
}
override val ignoredUsersFlow = mxCallbackFlow<ImmutableList<UserId>> {
// Fetch the initial value manually, the SDK won't return it automatically
channel.trySend(innerClient.ignoredUsers().map(::UserId).toImmutableList())
@@ -303,8 +311,21 @@ class RustMatrixClient(
sessionCoroutineScope.launch {
// Start notification settings
notificationSettingsService.start()
// Setup user profile logic
setupUserProfile()
}
// Update the user profile in the session store if needed
// Schedule regular database vacuuming to ensure DB performance remains optimal
scheduleDatabaseVacuum()
}
private suspend fun setupUserProfile() {
val supported = isUserStatusSupported().getOrDefault(false)
if (supported) {
// No need to seed the data here, it's already stored by the sdk.
ownProfileTaskHandle = innerClient.subscribeToOwnProfile(ownProfileListener)
} else {
// Seed from the session store so cold-start UI has something to render before the network fetch.
sessionStore.getSession(sessionId.value)?.let { sessionData ->
_userProfile.emit(
MatrixUser(
@@ -314,12 +335,8 @@ class RustMatrixClient(
)
)
}
// Force a refresh of the profile
getUserProfile()
}
// Schedule regular database vacuuming to ensure DB performance remains optimal
scheduleDatabaseVacuum()
}
override fun userIdServerName(): String {
@@ -458,13 +475,8 @@ class RustMatrixClient(
override suspend fun getUserProfile(): Result<MatrixUser> = getProfile(sessionId)
.onSuccess { matrixUser ->
_userProfile.emit(
matrixUser.copy(
rawStatus = localUserStatus,
displayedStatus = localUserStatus?.let { DisplayedStatus.UserSet(it) },
)
)
// Also update our session storage
_userProfile.emit(matrixUser)
sessionStore.updateUserProfile(
sessionId = sessionId.value,
displayName = matrixUser.displayName,
@@ -495,25 +507,31 @@ class RustMatrixClient(
}
override suspend fun setUserStatus(status: UserStatus): Result<Unit> = withContext(sessionDispatcher) {
localUserStatus = status
_userProfile.emit(
_userProfile.value.copy(
rawStatus = status,
displayedStatus = DisplayedStatus.UserSet(status),
)
)
Result.success(Unit)
runCatchingExceptions {
innerClient.setUserStatus(status.into())
}.onSuccess {
// The subscription pushes the update on supported servers; only refresh manually as a fallback.
if (ownProfileTaskHandle == null) getUserProfile()
}
}
override suspend fun clearUserStatus(): Result<Unit> = withContext(sessionDispatcher) {
localUserStatus = null
_userProfile.emit(
_userProfile.value.copy(
rawStatus = null,
displayedStatus = null,
)
)
Result.success(Unit)
runCatchingExceptions {
innerClient.clearUserStatus()
}.onSuccess {
// The subscription pushes the update on supported servers; only refresh manually as a fallback.
if (ownProfileTaskHandle == null) getUserProfile()
}
}
override suspend fun isUserStatusSupported(): Result<Boolean> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.isUserStatusSupported()
}
}
override fun enableAutomaticCallStatus(enabled: Boolean) {
innerClient.enableAutomaticCallStatus(enabled)
}
override suspend fun joinRoom(roomId: RoomId): Result<RoomInfo?> = withContext(sessionDispatcher) {
@@ -621,6 +639,7 @@ class RustMatrixClient(
sessionCoroutineScope.cancel()
clientDelegateTaskHandle?.cancelAndDestroy()
ownProfileTaskHandle?.cancelAndDestroy()
sessionVerificationService.destroy()
sessionDelegate.clearCurrentClient()
@@ -17,7 +17,7 @@ import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.UserId
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
@@ -70,7 +70,7 @@ class RustMatrixClientFactory(
private val clientBuilderProvider: ClientBuilderProvider,
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
private val workManagerScheduler: WorkManagerScheduler,
private val contentScannerUrlProvider: ContentScannerUrlProvider,
private val contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory,
) {
private val sessionDelegate = RustClientSessionDelegate(
sessionStore = sessionStore,
@@ -135,26 +135,25 @@ class RustMatrixClientFactory(
client.setUtdDelegate(UtdTracker(analyticsService))
val domainName = UserId(client.userId()).domainName
// 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 contentScanner = domainName?.let {
contentScannerUrlProvider.getContentScannerUrl(domainName)
.getOrNull()
?.let { contentScannerUrl ->
val contentScanner = ContentScanner(contentScannerUrl)
client.setContentScanner(contentScanner)
RustContentScanner(
client = client,
rustScanner = contentScanner,
)
}
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()
.withProfilesExtension()
.finish()
return RustMatrixClient(
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.TemporaryMatrixClient
import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.impl.exception.mapClientException
import org.matrix.rustcomponents.sdk.Client
class RustTemporaryMatrixClient(
private val client: Client,
private val paths: SessionPaths?,
) : TemporaryMatrixClient {
override suspend fun getUrl(url: String): Result<ByteArray> = runCatchingExceptions {
client.getUrl(url)
}.mapFailure { it.mapClientException() }
override fun close() {
client.close()
paths?.let {
if (it.fileDirectory.exists()) {
it.fileDirectory.deleteRecursively()
}
if (it.cacheDirectory.exists()) {
it.cacheDirectory.deleteRecursively()
}
}
}
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.TemporaryMatrixClient
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
@ContributesBinding(AppScope::class)
class RustTemporaryMatrixClientFactory(
private val sessionPathsFactory: SessionPathsFactory,
private val rustMatrixClientFactory: RustMatrixClientFactory,
) : TemporaryMatrixClientFactory {
override suspend fun create(homeServerUrl: String): Result<TemporaryMatrixClient> {
return runCatchingExceptions {
val sessionPaths = sessionPathsFactory.create()
val client = rustMatrixClientFactory.getBaseClientBuilder(
sessionPaths = sessionPaths,
clientSecret = null,
slidingSyncType = ClientBuilderSlidingSync.Native,
isMessageSearchAvailable = false,
)
.homeserverUrl(homeServerUrl)
.build()
RustTemporaryMatrixClient(client, sessionPaths)
}
}
}
@@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.RustTemporaryMatrixClient
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
@@ -133,9 +134,10 @@ class RustMatrixAuthenticationService(
}
currentClient = client
client.homeserverLoginDetails().map()
}.onFailure {
clear()
clear(destroyClient = true)
}.mapFailure { failure ->
Timber.e(failure, "Failed to set homeserver to $homeserver")
failure.mapAuthenticationException()
@@ -168,7 +170,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary
currentClient = null
clear(destroyClient = false)
SessionId(sessionData.userId)
}.mapFailure { failure ->
@@ -248,7 +250,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary
currentClient = null
clear(destroyClient = false)
SessionId(sessionData.userId)
}
@@ -271,11 +273,13 @@ class RustMatrixAuthenticationService(
deviceId = null,
additionalScopes = emptyList(),
)
val getUrlResolver = RustTemporaryMatrixClient(client, sessionPaths)
val url = oAuthAuthorizationData.loginUrl()
.let {
enterpriseService.tweakMasUrl(
url = it,
homeserver = client.server() ?: client.homeserver(),
urlContentFetcher = getUrlResolver,
)
}
pendingOAuthAuthorizationData = oAuthAuthorizationData
@@ -335,7 +339,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary
currentClient = null
clear(destroyClient = false)
SessionId(sessionData.userId)
}.mapFailure { failure ->
@@ -398,7 +402,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary
currentClient = null
clear(destroyClient = false)
SessionId(sessionData.userId)
}.mapFailure {
@@ -464,8 +468,10 @@ class RustMatrixAuthenticationService(
.build()
}
private fun clear() {
currentClient?.close()
private fun clear(destroyClient: Boolean) {
if (destroyClient) {
currentClient?.close()
}
currentClient = null
}
@@ -15,6 +15,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
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
@@ -108,4 +109,9 @@ object SessionMatrixModule {
fun providesContentScanner(matrixClient: MatrixClient): ContentScanner? {
return matrixClient.contentScanner
}
@Provides
fun providesGetUrlResolverClient(matrixClient: MatrixClient): UrlContentFetcher {
return matrixClient
}
}
@@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.impl.room.threads.RustThreadsListServ
import io.element.android.libraries.matrix.impl.roomdirectory.map
import io.element.android.libraries.matrix.impl.timeline.RustTimeline
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.user.map
import io.element.android.libraries.matrix.impl.util.MessageEventContent
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
@@ -16,12 +16,15 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.room.history.map
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsValuesMapper
import io.element.android.libraries.matrix.impl.room.tombstone.map
import io.element.android.libraries.matrix.impl.user.from
import io.element.android.libraries.matrix.impl.user.into
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import org.matrix.rustcomponents.sdk.Membership
@@ -102,7 +105,9 @@ fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {
fun RoomHero.map(): MatrixUser = MatrixUser(
userId = UserId(userId),
displayName = displayName,
avatarUrl = avatarUrl
avatarUrl = avatarUrl,
displayedStatus = DisplayedStatus.from(status, call),
rawStatus = status?.into(),
)
fun mapPowerLevels(roomPowerLevels: RustRoomPowerLevels): RoomPowerLevels {
@@ -11,7 +11,9 @@ package io.element.android.libraries.matrix.impl.room.member
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.impl.room.powerlevels.into
import io.element.android.libraries.matrix.impl.user.from
import uniffi.matrix_sdk.RoomMemberRole
import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember
@@ -30,8 +32,7 @@ object RoomMemberMapper {
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
membershipChangeReason = roomMember.membershipChangeReason,
isServiceMember = roomMember.isServiceMember,
// TODO map this value when available
displayedStatus = null,
displayedStatus = DisplayedStatus.from(roomMember.status, roomMember.call),
)
}
@@ -1,20 +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.impl.scanner
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
/**
* Default FOSS implementation of [ContentScannerUrlProvider] that returns `null` for the content scanner URL.
*/
@ContributesBinding(AppScope::class)
class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
override suspend fun getContentScannerUrl(homeserver: String): Result<String?> = Result.success(null)
}
@@ -21,7 +21,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetail
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.impl.core.RustSendHandle
import io.element.android.libraries.matrix.impl.user.from
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -75,8 +77,7 @@ fun RustProfileDetails.map(): ProfileDetails {
displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl,
// TODO map this value when available
displayedStatus = null,
displayedStatus = DisplayedStatus.from(status, call),
)
}
}
@@ -6,9 +6,10 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.mapper
package io.element.android.libraries.matrix.impl.user
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser
import org.matrix.rustcomponents.sdk.UserProfile
@@ -16,4 +17,6 @@ fun UserProfile.map() = MatrixUser(
userId = UserId(userId),
displayName = displayName,
avatarUrl = avatarUrl,
rawStatus = status?.into(),
displayedStatus = DisplayedStatus.from(status, call)
)
@@ -6,10 +6,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.usersearch
package io.element.android.libraries.matrix.impl.user
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.impl.mapper.map
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.SearchUsersResults
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.user
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.UserStatus
import org.matrix.rustcomponents.sdk.UserCall
import org.matrix.rustcomponents.sdk.UserStatus as RustUserStatus
fun UserStatus.into(): RustUserStatus {
return RustUserStatus(emoji = emoji, text = text)
}
fun RustUserStatus.into(): UserStatus {
return UserStatus(emoji = emoji, text = text)
}
fun DisplayedStatus.Companion.from(userStatus: RustUserStatus?, userCall: UserCall?): DisplayedStatus? {
return when {
userStatus != null -> DisplayedStatus.UserSet(userStatus.into())
userCall != null -> DisplayedStatus.InCall(userCall.callJoinedTs?.toLong())
else -> null
}
}
@@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.matrix.impl.mapper.map
import io.element.android.libraries.matrix.impl.user.map
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
@@ -52,7 +52,7 @@ fun TestScope.createRustMatrixClientFactory(
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
contentScannerUrlProvider: ContentScannerUrlProvider = { Result.success(null) },
contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } },
) = RustMatrixClientFactory(
cacheDirectory = cacheDirectory,
appCoroutineScope = backgroundScope,
@@ -67,5 +67,5 @@ fun TestScope.createRustMatrixClientFactory(
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler,
contentScannerUrlProvider = contentScannerUrlProvider,
contentScannerUrlProviderFactory = contentScannerUrlProviderFactory,
)
@@ -32,9 +32,9 @@ import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.Client
@@ -75,7 +75,8 @@ class RustMatrixClientTest {
@Test
fun `retrieving the UserProfile updates the database`() = runTest {
val updateUserProfileResult = lambdaRecorder<String, String?, String?, Unit> { _, _, _ -> }
val profilePersisted = CompletableDeferred<Unit>()
val updateUserProfileResult = lambdaRecorder<String, String?, String?, Unit> { _, _, _ -> profilePersisted.complete(Unit) }
val sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(
@@ -100,7 +101,7 @@ class RustMatrixClientTest {
),
sessionStore = sessionStore,
)
advanceUntilIdle()
profilePersisted.await()
updateUserProfileResult.assertions().isCalledOnce()
.with(
value(A_USER_ID.value),
@@ -0,0 +1,84 @@
/*
* 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 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
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
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.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.File
class RustTemporaryMatrixClientFactoryTest {
@Test
fun `create returns a TemporaryMatrixClient`() = runTest {
val sut = createRustTemporaryMatrixClientFactory()
val result = sut.create("https://matrix.org")
assertThat(result.isSuccess).isTrue()
}
@Test
fun `create can fail gracefully`() = runTest {
val sut = createRustTemporaryMatrixClientFactory(
rustMatrixClientFactory = createRustMatrixClientFactory(
clientBuilderProvider = FakeClientBuilderProvider(
provideResult = { throw IllegalStateException("Failed to create client builder") }
)
)
)
val result = sut.create("https://matrix.org")
assertThat(result.isFailure).isTrue()
}
private fun TestScope.createRustTemporaryMatrixClientFactory(
sessionPathsFactory: SessionPathsFactory = SessionPathsFactory(File("/base"), File("/cache")),
rustMatrixClientFactory: RustMatrixClientFactory = createRustMatrixClientFactory(),
): RustTemporaryMatrixClientFactory {
return RustTemporaryMatrixClientFactory(
sessionPathsFactory = sessionPathsFactory,
rustMatrixClientFactory = rustMatrixClientFactory,
)
}
private fun TestScope.createRustMatrixClientFactory(
cacheDirectory: File = File("/cache"),
sessionStore: SessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } },
) = RustMatrixClientFactory(
cacheDirectory = cacheDirectory,
appCoroutineScope = backgroundScope,
coroutineDispatchers = testCoroutineDispatchers(),
sessionStore = sessionStore,
userAgentProvider = SimpleUserAgentProvider(),
proxyProvider = FakeProxyProvider(),
clock = FakeSystemClock(),
analyticsService = FakeAnalyticsService(),
featureFlagService = FakeFeatureFlagService(),
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler,
contentScannerUrlProviderFactory = contentScannerUrlProviderFactory,
)
}
@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvide
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.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -50,6 +51,29 @@ class RustMatrixAuthenticationServiceTest {
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
}
@Test
fun `setHomeserver can fail gracefully and clean up the temporary client`() = runTest {
val closeResult = lambdaRecorder<Unit> {}
val sut = createRustMatrixAuthenticationService(
clientBuilderProvider = FakeClientBuilderProvider(
provideResult = {
FakeFfiClientBuilder(
buildResult = {
FakeFfiClient(
homeserverLoginDetailsResult = {
throw IllegalStateException("Failed to get homeserver login details")
},
closeResult = closeResult,
)
},
)
},
),
)
assertThat(sut.setHomeserver("matrix.org").isFailure).isTrue()
closeResult.assertions().isCalledOnce()
}
private fun TestScope.createRustMatrixAuthenticationService(
sessionStore: SessionStore = InMemorySessionStore(),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
@@ -26,6 +26,7 @@ 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.ProfileListener
import org.matrix.rustcomponents.sdk.PusherIdentifiers
import org.matrix.rustcomponents.sdk.PusherKind
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
@@ -57,6 +58,8 @@ class FakeFfiClient(
private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(),
private val accountDataResult: (String) -> String? = { lambdaError() },
private val setAccountDataResult: (String, String) -> Unit = { _, _ -> lambdaError() },
private val isUserStatusSupportedResult: () -> Boolean = { false },
private val subscribeToOwnProfileResult: (ProfileListener) -> Unit = {},
private val closeResult: () -> Unit = {},
) : Client(NoHandle) {
override fun userId(): String = userId
@@ -94,6 +97,13 @@ class FakeFfiClient(
return FakeFfiTaskHandle()
}
override suspend fun isUserStatusSupported(): Boolean = isUserStatusSupportedResult()
override fun subscribeToOwnProfile(listener: ProfileListener): TaskHandle {
subscribeToOwnProfileResult(listener)
return FakeFfiTaskHandle()
}
override suspend fun getProfile(userId: String): UserProfile {
return getProfileResult(userId)
}
@@ -15,5 +15,6 @@ import org.matrix.rustcomponents.sdk.SyncServiceBuilder
class FakeFfiSyncServiceBuilder : SyncServiceBuilder(NoHandle) {
override fun withOfflineMode(): SyncServiceBuilder = this
override fun withSharePos(enable: Boolean): SyncServiceBuilder = this
override fun withProfilesExtension(): SyncServiceBuilder = this
override suspend fun finish(): SyncService = FakeFfiSyncService()
}
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.mapper
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
import io.element.android.libraries.matrix.impl.user.map
import io.element.android.libraries.matrix.test.A_USER_ID
import org.junit.Test
@@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSearchUsersResults
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
import io.element.android.libraries.matrix.impl.user.UserSearchResultMapper
import io.element.android.libraries.matrix.test.A_USER_ID
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test