Simplify syncing the room list when receiving a push (#4915)

This commit is contained in:
Jorge Martin Espinosa 2025-06-24 10:24:05 +02:00 committed by GitHub
parent ebda14b37b
commit 9c6aa97818
2 changed files with 7 additions and 62 deletions

View file

@ -8,21 +8,15 @@
package io.element.android.libraries.push.impl.push package io.element.android.libraries.push.impl.push
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.appnavstate.api.AppForegroundStateService
import kotlinx.coroutines.flow.first import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SyncOnNotifiableEvent @Inject constructor( class SyncOnNotifiableEvent @Inject constructor(
@ -30,7 +24,6 @@ class SyncOnNotifiableEvent @Inject constructor(
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val appForegroundStateService: AppForegroundStateService, private val appForegroundStateService: AppForegroundStateService,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val activeRoomsHolder: ActiveRoomsHolder,
) { ) {
suspend operator fun invoke(notifiableEvents: List<NotifiableEvent>) = withContext(dispatchers.io) { suspend operator fun invoke(notifiableEvents: List<NotifiableEvent>) = withContext(dispatchers.io) {
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) { if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) {
@ -41,6 +34,7 @@ class SyncOnNotifiableEvent @Inject constructor(
val eventsBySession = notifiableEvents.groupBy { it.sessionId } val eventsBySession = notifiableEvents.groupBy { it.sessionId }
appForegroundStateService.updateIsSyncingNotificationEvent(true) appForegroundStateService.updateIsSyncingNotificationEvent(true)
Timber.d("Starting opportunistic room list sync | In foreground: ${appForegroundStateService.isInForeground.value}")
for ((sessionId, events) in eventsBySession) { for ((sessionId, events) in eventsBySession) {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: continue val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: continue
@ -49,38 +43,13 @@ class SyncOnNotifiableEvent @Inject constructor(
client.roomListService.subscribeToVisibleRooms(eventsByRoomId.keys.toList()) client.roomListService.subscribeToVisibleRooms(eventsByRoomId.keys.toList())
if (!appForegroundStateService.isInForeground.value) { if (!appForegroundStateService.isInForeground.value) {
for ((roomId, eventsInRoom) in eventsByRoomId) { // Give the sync some time to complete in background
val activeRoom = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId) delay(10.seconds)
val room = activeRoom ?: client.getJoinedRoom(roomId)
if (room != null) {
eventsInRoom.parallelMap { event ->
room.waitsUntilEventIsKnown(event.eventId, timeout = 10.seconds)
}
}
if (room != null && activeRoom == null) {
// Destroy the room we just instantiated to reset its live timeline
room.destroy()
}
}
} }
} }
} finally { } finally {
Timber.d("Finished opportunistic room list sync")
appForegroundStateService.updateIsSyncingNotificationEvent(false) appForegroundStateService.updateIsSyncingNotificationEvent(false)
} }
} }
private suspend fun JoinedRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) {
withTimeoutOrNull(timeout) {
liveTimeline.timelineItems.first { timelineItems ->
timelineItems.any { timelineItem ->
when (timelineItem) {
is MatrixTimelineItem.Event -> timelineItem.eventId == eventId
else -> false
}
}
}
}
}
} }

View file

@ -13,27 +13,20 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
@ -43,16 +36,11 @@ import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SyncOnNotifiableEventTest { class SyncOnNotifiableEventTest {
private val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList())
private val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) } private val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
private val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) } private val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
private val subscribeToSyncLambda = lambdaRecorder<Unit> { } private val subscribeToSyncLambda = lambdaRecorder<Unit> { }
private val liveTimeline = FakeTimeline(
timelineItems = timelineItems,
)
private val room = FakeJoinedRoom( private val room = FakeJoinedRoom(
liveTimeline = liveTimeline,
baseRoom = FakeBaseRoom( baseRoom = FakeBaseRoom(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
subscribeToSyncLambda = subscribeToSyncLambda, subscribeToSyncLambda = subscribeToSyncLambda,
@ -130,10 +118,6 @@ class SyncOnNotifiableEventTest {
) )
val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true) val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true)
timelineItems.emit(
listOf(MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()))
)
appForegroundStateService.isSyncingNotificationEvent.test { appForegroundStateService.isSyncingNotificationEvent.test {
syncService.emitSyncState(SyncState.Running) syncService.emitSyncState(SyncState.Running)
sut(listOf(notifiableEvent)) sut(listOf(notifiableEvent))
@ -150,7 +134,7 @@ class SyncOnNotifiableEventTest {
} }
@Test @Test
fun `when feature flag is enabled and app is in background, running multiple time only call once`() = runTest { fun `when feature flag is enabled and app is in background, running multiple times only call once`() = runTest {
val appForegroundStateService = FakeAppForegroundStateService( val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = false, initialForegroundValue = false,
) )
@ -159,12 +143,6 @@ class SyncOnNotifiableEventTest {
appForegroundStateService.isSyncingNotificationEvent.test { appForegroundStateService.isSyncingNotificationEvent.test {
launch { sut(listOf(notifiableEvent)) } launch { sut(listOf(notifiableEvent)) }
launch { sut(listOf(notifiableEvent)) } launch { sut(listOf(notifiableEvent)) }
launch {
delay(1)
timelineItems.emit(
listOf(MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()))
)
}
// It's initially false // It's initially false
assertThat(awaitItem()).isFalse() assertThat(awaitItem()).isFalse()
@ -183,7 +161,6 @@ class SyncOnNotifiableEventTest {
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService( appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = true, initialForegroundValue = true,
), ),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
): SyncOnNotifiableEvent { ): SyncOnNotifiableEvent {
val featureFlagService = FakeFeatureFlagService( val featureFlagService = FakeFeatureFlagService(
initialState = mapOf( initialState = mapOf(
@ -196,7 +173,6 @@ class SyncOnNotifiableEventTest {
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
activeRoomsHolder = activeRoomsHolder,
) )
} }
} }