Merge branch 'develop' into feature/fga/fix_left_room_membership_change

This commit is contained in:
ganfra 2025-05-27 22:08:17 +02:00
commit 091d41b09d
153 changed files with 2885 additions and 1088 deletions

View file

@ -120,10 +120,6 @@ class RustClientSessionDelegate(
}
}
override fun didRefreshTokens() {
// This is done in `saveSessionInKeychain(Session)` instead.
}
override fun retrieveSessionFromKeychain(userId: String): Session {
// This should never be called, as it's only used for multi-process setups
error("retrieveSessionFromKeychain should never be called for Android")

View file

@ -152,7 +152,7 @@ class RustMatrixClient(
)
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService)
private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) }
private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock)
private val notificationService = RustNotificationService(sessionId, innerNotificationClient, dispatchers, clock)
private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
private val encryptionService = RustEncryptionService(
client = innerClient,
@ -189,7 +189,6 @@ class RustMatrixClient(
private val roomMembershipObserver = RoomMembershipObserver()
private val roomFactory = RustRoomFactory(
innerClient = innerClient,
roomListService = roomListService,
innerRoomListService = innerRoomListService,
sessionId = sessionId,

View file

@ -72,8 +72,9 @@ class RustMatrixClientFactory @Inject constructor(
suspend fun create(client: Client): RustMatrixClient {
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
client.setUtdDelegate(UtdTracker(analyticsService))
val syncService = client.syncService()
.withUtdHook(UtdTracker(analyticsService))
.withOfflineMode()
.finish()

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
@ -25,6 +26,7 @@ class NotificationMapper(
private val notificationContentMapper = NotificationContentMapper()
fun map(
sessionId: SessionId,
eventId: EventId,
roomId: RoomId,
notificationItem: NotificationItem
@ -35,6 +37,7 @@ class NotificationMapper(
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
)
NotificationData(
sessionId = sessionId,
eventId = eventId,
// FIXME once the `NotificationItem` in the SDK returns the thread id
threadId = null,

View file

@ -10,28 +10,45 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.NotificationItemsRequest
import timber.log.Timber
class RustNotificationService(
private val sessionId: SessionId,
private val notificationClient: NotificationClient,
private val dispatchers: CoroutineDispatchers,
clock: SystemClock,
) : NotificationService {
private val notificationMapper: NotificationMapper = NotificationMapper(clock)
override suspend fun getNotification(
roomId: RoomId,
eventId: EventId,
): Result<NotificationData?> = withContext(dispatchers.io) {
override suspend fun getNotifications(
ids: Map<RoomId, List<EventId>>
): Result<Map<EventId, NotificationData>> = withContext(dispatchers.io) {
runCatching {
val item = notificationClient.getNotification(roomId.value, eventId.value)
item?.use {
notificationMapper.map(eventId, roomId, it)
val requests = ids.map { (roomId, eventIds) ->
NotificationItemsRequest(
roomId = roomId.value,
eventIds = eventIds.map { it.value }
)
}
val items = notificationClient.getNotifications(requests)
buildMap {
val eventIds = requests.flatMap { it.eventIds }
for (eventId in eventIds) {
val item = items[eventId]
if (item != null) {
val roomId = RoomId(requests.find { it.eventIds.contains(eventId) }?.roomId!!)
put(EventId(eventId), notificationMapper.map(sessionId, EventId(eventId), roomId, item))
} else {
Timber.e("Could not retrieve event for notification with $eventId")
}
}
}
}
}

View file

@ -8,7 +8,9 @@
package io.element.android.libraries.matrix.impl.proxy
import android.content.Context
import android.net.ConnectivityManager
import android.provider.Settings
import androidx.core.content.getSystemService
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@ -32,6 +34,13 @@ class DefaultProxyProvider @Inject constructor(
private val context: Context
) : ProxyProvider {
override fun provides(): String? {
val defaultProxy = context.getSystemService<ConnectivityManager>()?.defaultProxy
if (defaultProxy == null) {
// Note: can be tested by running:
// adb shell settings put global http_proxy :0
Timber.d("No default proxy")
return null
}
return Settings.Global.getString(context.contentResolver, Settings.Global.HTTP_PROXY)
?.also {
Timber.d("Using global proxy")

View file

@ -212,6 +212,7 @@ class JoinedRustRoom(
internalIdPrefix = internalIdPrefix,
dateDividerMode = dateDividerMode,
trackReadReceipts = trackReadReceipts,
reportUtds = true,
)
).let { innerTimeline ->
val mode = when (createTimelineParams) {
@ -416,7 +417,6 @@ class JoinedRustRoom(
RustWidgetDriver(
widgetSettings = widgetSettings,
room = innerRoom,
joinedRustRoom = this,
widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider {
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
return getElementCallRequiredPermissions(sessionId.value, baseRoom.deviceId.value)

View file

@ -11,7 +11,6 @@ import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.ForwardEventException
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered
import kotlinx.coroutines.CancellationException
@ -49,10 +48,7 @@ class RoomContentForwarder(
val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content
?: throw ForwardEventException(toRoomIds)
val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
val targetRooms = targetSlidingSyncRooms.map { slidingSyncRoom ->
slidingSyncRoom.use { it.fullRoomWithTimeline(null) }
}
val targetRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
val failedForwardingTo = mutableSetOf<RoomId>()
targetRooms.parallelMap { room ->
room.use { targetRoom ->

View file

@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@ -28,10 +27,12 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.DateDividerMode
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TimelineConfiguration
import org.matrix.rustcomponents.sdk.TimelineFilter
import org.matrix.rustcomponents.sdk.TimelineFocus
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
@ -39,7 +40,6 @@ import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
class RustRoomFactory(
private val sessionId: SessionId,
private val deviceId: DeviceId,
private val innerClient: Client,
private val notificationSettingsService: NotificationSettingsService,
private val sessionCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
@ -79,16 +79,11 @@ class RustRoomFactory(
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
getBaseRoom(roomListItem)
val room = awaitRoomInRoomList(roomId) ?: return@withContext null
getBaseRoom(room)
}
}
private suspend fun getBaseRoom(roomListItem: RoomListItem): RustBaseRoom? {
val sdkRoom = innerClient.getRoom(roomListItem.id()) ?: return null
return getBaseRoom(sdkRoom)
}
private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom {
val initialRoomInfo = sdkRoom.roomInfo()
return RustBaseRoom(
@ -110,18 +105,27 @@ class RustRoomFactory(
Timber.d("Room factory is destroyed, returning null for $roomId")
return@withContext null
}
val roomListItem = awaitRoomListItem(roomId) ?: return@withContext null
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null
if (roomListItem.membership() == Membership.JOINED) {
// Init the live timeline in the SDK from the RoomListItem
val sdkRoom = roomListItem.fullRoomWithTimeline(eventFilters)
if (sdkRoom.membership() == Membership.JOINED) {
// Init the live timeline in the SDK from the Room
val timeline = sdkRoom.timelineWithConfiguration(
TimelineConfiguration(
focus = TimelineFocus.Live,
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
internalIdPrefix = "live",
dateDividerMode = DateDividerMode.DAILY,
trackReadReceipts = true,
reportUtds = true,
)
)
GetRoomResult.Joined(
JoinedRustRoom(
baseRoom = getBaseRoom(sdkRoom),
notificationSettingsService = notificationSettingsService,
roomContentForwarder = roomContentForwarder,
liveInnerTimeline = sdkRoom.timeline(),
liveInnerTimeline = timeline,
coroutineDispatchers = dispatchers,
systemClock = systemClock,
featureFlagService = featureFlagService,
@ -129,7 +133,7 @@ class RustRoomFactory(
)
} else {
val preview = try {
roomListItem.previewRoom(via = emptyList())
sdkRoom.previewRoom(via = emptyList())
} catch (e: Exception) {
Timber.e(e, "Failed to get room preview for $roomId")
return@withContext null
@ -138,7 +142,7 @@ class RustRoomFactory(
GetRoomResult.NotJoined(
NotJoinedRustRoom(
sessionId = sessionId,
localRoom = getBaseRoom(roomListItem),
localRoom = getBaseRoom(sdkRoom),
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
)
)
@ -147,22 +151,22 @@ class RustRoomFactory(
}
/**
* Get the Rust room list item for a room, retrying after the room list is loaded if necessary.
* Get the Rust room for a room, retrying after the room list is loaded if necessary.
*/
private suspend fun awaitRoomListItem(roomId: RoomId): RoomListItem? {
var roomListItem = innerRoomListService.roomOrNull(roomId.value)
if (roomListItem == null) {
private suspend fun awaitRoomInRoomList(roomId: RoomId): Room? {
var sdkRoom = innerRoomListService.roomOrNull(roomId.value)
if (sdkRoom == null) {
// ... otherwise, lets wait for the SS to load all rooms and check again.
roomListService.allRooms.awaitLoaded()
roomListItem = innerRoomListService.roomOrNull(roomId.value)
sdkRoom = innerRoomListService.roomOrNull(roomId.value)
}
if (roomListItem == null) {
if (sdkRoom == null) {
Timber.d("Room not found for $roomId")
return null
}
return roomListItem
return sdkRoom
}
}

View file

@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListInterface
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
@ -114,7 +114,7 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncI
)
}.buffer(Channel.UNLIMITED)
internal fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
internal fun RoomListServiceInterface.roomOrNull(roomId: String): Room? {
return tryOrNull(
onError = { Timber.e(it, "Failed finding room with id=$roomId.") }
) {

View file

@ -1,20 +0,0 @@
/*
* Copyright 2024 New Vector 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.roomlist
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
/** Returns a `Room` with an initialized timeline using the given [filter]. */
suspend fun RoomListItem.fullRoomWithTimeline(filter: TimelineEventTypeFilter? = null): Room {
if (!isTimelineInitialized()) {
initTimeline(filter, "live")
}
return fullRoom()
}

View file

@ -10,16 +10,16 @@ package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.room.RoomInfoMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.use
class RoomSummaryFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
private val roomInfoMapper: RoomInfoMapper = RoomInfoMapper(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map)
val latestRoomMessage = roomListItem.latestEvent().use { event ->
suspend fun create(room: Room): RoomSummary {
val roomInfo = room.roomInfo().let(roomInfoMapper::map)
val latestRoomMessage = room.latestEvent().use { event ->
roomMessageFactory.create(event)
}
return RoomSummary(

View file

@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
@ -95,20 +95,16 @@ class RoomSummaryListProcessor(
}
}
private suspend fun buildSummaryForRoomListEntry(entry: RoomListItem): RoomSummary {
return buildRoomSummaryForRoomListItem(entry)
private suspend fun buildSummaryForRoomListEntry(entry: Room): RoomSummary {
return entry.use { roomSummaryDetailsFactory.create(room = it) }
}
private suspend fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary? {
return roomListService.roomOrNull(identifier)?.use { roomListItem ->
buildRoomSummaryForRoomListItem(roomListItem)
return roomListService.roomOrNull(identifier)?.let { room ->
buildSummaryForRoomListEntry(room)
}
}
private suspend fun buildRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary {
return roomSummaryDetailsFactory.create(roomListItem = roomListItem)
}
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
mutex.withLock {
val current = roomSummaries.replayCache.lastOrNull()

View file

@ -53,4 +53,5 @@ fun TracingConfiguration.map(): org.matrix.rustcomponents.sdk.TracingConfigurati
extraTargets = extraTargets,
traceLogPacks = traceLogPacks.map(),
writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(),
sentryDsn = null,
)

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl.widget
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.room.JoinedRustRoom
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -25,7 +24,6 @@ import kotlin.coroutines.coroutineContext
class RustWidgetDriver(
widgetSettings: MatrixWidgetSettings,
private val room: Room,
private val joinedRustRoom: JoinedRustRoom,
private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider,
) : MatrixWidgetDriver {
// It's important to have extra capacity here to make sure we don't drop any messages
@ -71,6 +69,5 @@ class RustWidgetDriver(
override fun close() {
receiveMessageJob?.cancel()
driverAndHandle.driver.close()
joinedRustRoom.destroy()
}
}

View file

@ -25,6 +25,7 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate
class FakeRustClient(
private val userId: String = A_USER_ID.value,
@ -34,6 +35,7 @@ class FakeRustClient(
private val encryption: Encryption = FakeRustEncryption(),
private val session: Session = aRustSession(),
private val clearCachesResult: () -> Unit = { lambdaError() },
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
private val closeResult: () -> Unit = {},
) : Client(NoPointer) {
override fun userId(): String = userId
@ -58,5 +60,6 @@ class FakeRustClient(
override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit
override suspend fun clearCaches() = simulateLongTask { clearCachesResult() }
override suspend fun setUtdDelegate(utdDelegate: UnableToDecryptDelegate) = withUtdHook(utdDelegate)
override fun close() = closeResult()
}

View file

@ -47,6 +47,6 @@ class FakeRustClientBuilder : ClientBuilder(NoPointer) {
}
override suspend fun build(): Client {
return FakeRustClient()
return FakeRustClient(withUtdHook = {})
}
}

View file

@ -7,15 +7,15 @@
package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.tests.testutils.simulateLongTask
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.NotificationItemsRequest
class FakeRustNotificationClient(
var notificationItemResult: NotificationItem? = null
var notificationItemResult: Map<String, NotificationItem> = emptyMap(),
) : NotificationClient(NoPointer) {
override suspend fun getNotification(roomId: String, eventId: String): NotificationItem? = simulateLongTask {
notificationItemResult
override suspend fun getNotifications(requests: List<NotificationItemsRequest>): Map<String, NotificationItem> {
return notificationItemResult
}
}

View file

@ -8,10 +8,13 @@
package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.tests.testutils.lambda.lambdaError
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomMembersIterator
class FakeRustRoom(
@ -19,6 +22,8 @@ class FakeRustRoom(
private val getMembers: () -> RoomMembersIterator = { lambdaError() },
private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() },
private val leaveLambda: () -> Unit = { lambdaError() },
private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() },
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
) : Room(NoPointer) {
override fun id(): String {
return roomId.value
@ -36,6 +41,14 @@ class FakeRustRoom(
leaveLambda()
}
override suspend fun roomInfo(): RoomInfo {
return roomInfo
}
override suspend fun latestEvent(): EventTimelineItem? {
return latestEventLambda()
}
override fun close() {
// No-op
}

View file

@ -1,33 +0,0 @@
/*
* Copyright 2024 New Vector 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 io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomListItem
class FakeRustRoomListItem(
private val roomId: RoomId,
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
private val latestEvent: EventTimelineItem? = null,
) : RoomListItem(NoPointer) {
override fun id(): String {
return roomId.value
}
override suspend fun roomInfo(): RoomInfo {
return roomInfo
}
override suspend fun latestEvent(): EventTimelineItem? {
return latestEvent
}
}

View file

@ -10,10 +10,8 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.SyncService
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate
class FakeRustSyncServiceBuilder : SyncServiceBuilder(NoPointer) {
override suspend fun withUtdHook(delegate: UnableToDecryptDelegate): SyncServiceBuilder = this
override fun withOfflineMode(): SyncServiceBuilder = this
override suspend fun finish(): SyncService = FakeRustSyncService()
}

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificat
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
@ -28,12 +29,12 @@ class RustNotificationServiceTest {
@Test
fun test() = runTest {
val notificationClient = FakeRustNotificationClient(
notificationItemResult = aRustNotificationItem(),
notificationItemResult = mapOf(AN_EVENT_ID.value to aRustNotificationItem()),
)
val sut = createRustNotificationService(
notificationClient = notificationClient,
)
val result = sut.getNotification(A_ROOM_ID, AN_EVENT_ID).getOrThrow()!!
val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!!
assertThat(result.isEncrypted).isTrue()
assertThat(result.content).isEqualTo(
NotificationContent.MessageLike.RoomMessage(
@ -51,6 +52,7 @@ class RustNotificationServiceTest {
clock: SystemClock = FakeSystemClock(),
) =
RustNotificationService(
sessionId = A_SESSION_ID,
notificationClient = notificationClient,
dispatchers = testCoroutineDispatchers(),
clock = clock,

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.room
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@ -19,7 +20,6 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.isActive
@ -30,10 +30,7 @@ import org.junit.Test
class RustBaseRoomTest {
@Test
fun `RustBaseRoom should cancel the room coroutine scope when it is destroyed`() = runTest {
val rustBaseRoom = createRustBaseRoom(
// Not using backgroundScope here, but the test scope
sessionCoroutineScope = this
)
val rustBaseRoom = createRustBaseRoom()
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isTrue()
rustBaseRoom.destroy()
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
@ -43,7 +40,6 @@ class RustBaseRoomTest {
fun `when currentUserMembership=JOINED and user leave room succeed then roomMembershipObserver emits change as LEFT`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
sessionCoroutineScope = this,
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED),
innerRoom = FakeRustRoom(
leaveLambda = {
@ -52,23 +48,18 @@ class RustBaseRoomTest {
),
roomMembershipObserver = roomMembershipObserver,
)
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
rustBaseRoom.leave()
shared.test {
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT)
ensureAllEventsConsumed()
}
rustBaseRoom.destroy()
}
@Test
fun `when currentUserMembership=KNOCKED and user leave room succeed then roomMembershipObserver emits change as KNOCK_RETRACTED`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
sessionCoroutineScope = this,
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED),
innerRoom = FakeRustRoom(
leaveLambda = {
@ -77,23 +68,18 @@ class RustBaseRoomTest {
),
roomMembershipObserver = roomMembershipObserver,
)
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
rustBaseRoom.leave()
shared.test {
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED)
ensureAllEventsConsumed()
}
rustBaseRoom.destroy()
}
@Test
fun `when currentUserMembership=INVITED and user leave room succeed then roomMembershipObserver emits change as INVITATION_REJECTED`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
sessionCoroutineScope = this,
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
innerRoom = FakeRustRoom(
leaveLambda = {
@ -102,39 +88,44 @@ class RustBaseRoomTest {
),
roomMembershipObserver = roomMembershipObserver,
)
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
rustBaseRoom.leave()
shared.test {
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED)
ensureAllEventsConsumed()
}
rustBaseRoom.destroy()
}
@Test
fun `when user leave room fails then roomMembershipObserver emits nothing`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
sessionCoroutineScope = this,
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
innerRoom = FakeRustRoom(
leaveLambda = { error("Leave failed") }
),
roomMembershipObserver = roomMembershipObserver,
)
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
// No emit
}
}
private suspend fun TestScope.leaveRoomAndObserveMembershipChange(
roomMembershipObserver: RoomMembershipObserver,
rustBaseRoom: RustBaseRoom,
validate: suspend TurbineTestContext<RoomMembershipObserver.RoomMembershipUpdate>.() -> Unit
) {
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
rustBaseRoom.leave()
shared.test {
validate()
ensureAllEventsConsumed()
}
rustBaseRoom.destroy()
}
private fun TestScope.createRustBaseRoom(
sessionCoroutineScope: CoroutineScope,
initialRoomInfo: RoomInfo = aRoomInfo(),
innerRoom: FakeRustRoom = FakeRustRoom(),
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
@ -150,7 +141,8 @@ class RustBaseRoomTest {
dispatchers = dispatchers,
),
roomMembershipObserver = roomMembershipObserver,
sessionCoroutineScope = sessionCoroutineScope,
// Not using backgroundScope here, but the test scope
sessionCoroutineScope = this,
roomInfoMapper = RoomInfoMapper(),
initialRoomInfo = initialRoomInfo,
)

View file

@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomDirectorySearch
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@ -31,7 +30,6 @@ class RustBaseRoomDirectoryListTest {
val mapper = RoomDescriptionMapper()
val sut = createRustRoomDirectoryList(
roomDirectorySearch = roomDirectorySearch,
scope = backgroundScope,
)
// Let the mxCallback be ready
runCurrent()
@ -81,10 +79,9 @@ class RustBaseRoomDirectoryListTest {
private fun TestScope.createRustRoomDirectoryList(
roomDirectorySearch: RoomDirectorySearch = FakeRustRoomDirectorySearch(),
scope: CoroutineScope,
) = RustRoomDirectoryList(
inner = roomDirectorySearch,
coroutineScope = scope,
coroutineScope = backgroundScope,
coroutineContext = StandardTestDispatcher(testScheduler),
)
}

View file

@ -8,8 +8,9 @@
package io.element.android.libraries.matrix.impl.roomlist
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListItem
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
@ -30,7 +31,7 @@ class RoomSummaryListProcessorTest {
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
val newEntry = FakeRustRoomListItem(A_ROOM_ID_2)
val newEntry = aRustRoom(A_ROOM_ID_2)
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry))))
assertThat(summaries.value.count()).isEqualTo(4)
@ -41,7 +42,7 @@ class RoomSummaryListProcessorTest {
fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(aRustRoom(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_2)
@ -51,7 +52,7 @@ class RoomSummaryListProcessorTest {
fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(aRustRoom(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2)
@ -63,7 +64,7 @@ class RoomSummaryListProcessorTest {
val processor = createProcessor()
val index = 0
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), aRustRoom(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
@ -75,7 +76,7 @@ class RoomSummaryListProcessorTest {
val processor = createProcessor()
val index = 0
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), aRustRoom(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
@ -163,12 +164,17 @@ class RoomSummaryListProcessorTest {
val processor = createProcessor()
val index = 0
processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(FakeRustRoomListItem(A_ROOM_ID_3)))))
processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(aRustRoom(A_ROOM_ID_3)))))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3)
}
private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeRustRoom(
roomId = roomId,
latestEventLambda = { null },
)
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
summaries,
FakeRustRoomListService(),

View file

@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@ -28,7 +27,6 @@ class RustBaseRoomListServiceTest {
fun `syncIndicator should emit the expected values`() = runTest {
val roomListService = FakeRustRoomListService()
val sut = createRustRoomListService(
sessionCoroutineScope = backgroundScope,
roomListService = roomListService,
)
// Give time for mxCallback to setup
@ -44,18 +42,17 @@ class RustBaseRoomListServiceTest {
}
private fun TestScope.createRustRoomListService(
sessionCoroutineScope: CoroutineScope,
roomListService: RustRoomListService = FakeRustRoomListService(),
) = RustRoomListService(
innerRoomListService = roomListService,
sessionDispatcher = StandardTestDispatcher(testScheduler),
roomListFactory = RoomListFactory(
innerRoomListService = roomListService,
sessionCoroutineScope = sessionCoroutineScope,
sessionCoroutineScope = backgroundScope,
),
roomSyncSubscriber = RoomSyncSubscriber(
roomListService = roomListService,
dispatchers = testCoroutineDispatchers(),
),
sessionCoroutineScope = sessionCoroutineScope,
sessionCoroutineScope = backgroundScope,
)

View file

@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineI
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -38,7 +37,6 @@ class TimelineItemsSubscriberTest {
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
val timeline = FakeRustTimeline()
val timelineItemsSubscriber = createTimelineItemsSubscriber(
coroutineScope = backgroundScope,
timeline = timeline,
timelineItems = timelineItems,
)
@ -59,7 +57,6 @@ class TimelineItemsSubscriberTest {
MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
val timeline = FakeRustTimeline()
val timelineItemsSubscriber = createTimelineItemsSubscriber(
coroutineScope = backgroundScope,
timeline = timeline,
timelineItems = timelineItems,
)
@ -81,7 +78,6 @@ class TimelineItemsSubscriberTest {
val timeline = FakeRustTimeline()
val onNewSyncedEventRecorder = lambdaRecorder<Unit> { }
val timelineItemsSubscriber = createTimelineItemsSubscriber(
coroutineScope = backgroundScope,
timeline = timeline,
timelineItems = timelineItems,
onNewSyncedEvent = onNewSyncedEventRecorder,
@ -109,9 +105,7 @@ class TimelineItemsSubscriberTest {
@Test
fun `multiple subscriptions does not have side effect`() = runTest {
val timelineItemsSubscriber = createTimelineItemsSubscriber(
coroutineScope = backgroundScope,
)
val timelineItemsSubscriber = createTimelineItemsSubscriber()
timelineItemsSubscriber.subscribeIfNeeded()
timelineItemsSubscriber.subscribeIfNeeded()
timelineItemsSubscriber.unsubscribeIfNeeded()
@ -120,7 +114,6 @@ class TimelineItemsSubscriberTest {
}
private fun TestScope.createTimelineItemsSubscriber(
coroutineScope: CoroutineScope,
timeline: Timeline = FakeRustTimeline(),
timelineItems: MutableSharedFlow<List<MatrixTimelineItem>> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE),
initLatch: CompletableDeferred<Unit> = CompletableDeferred(),
@ -128,7 +121,7 @@ private fun TestScope.createTimelineItemsSubscriber(
onNewSyncedEvent: () -> Unit = { lambdaError() },
): TimelineItemsSubscriber {
return TimelineItemsSubscriber(
timelineCoroutineScope = coroutineScope,
timelineCoroutineScope = backgroundScope,
dispatcher = StandardTestDispatcher(testScheduler),
timeline = timeline,
timelineDiffProcessor = createMatrixTimelineDiffProcessor(timelineItems),