Skip to content

Commit 8a3489c

Browse files
authored
Merge pull request #3260 from element-hq/feature/fga/start_sync_on_push
Feature/fga/start sync on push
2 parents 0bcfffd + d8532c0 commit 8a3489c

File tree

10 files changed

+275
-36
lines changed

10 files changed

+275
-36
lines changed

features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,21 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
3636
import io.element.android.libraries.matrix.test.FakeMatrixClient
3737
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
3838
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
39+
import io.element.android.libraries.matrix.test.sync.FakeSyncService
3940
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
4041
import io.element.android.libraries.network.useragent.UserAgentProvider
4142
import io.element.android.services.analytics.api.ScreenTracker
4243
import io.element.android.services.analytics.test.FakeScreenTracker
4344
import io.element.android.services.toolbox.api.systemclock.SystemClock
4445
import io.element.android.tests.testutils.WarmUpRule
4546
import io.element.android.tests.testutils.consumeItemsUntilTimeout
47+
import io.element.android.tests.testutils.lambda.assert
4648
import io.element.android.tests.testutils.lambda.lambdaRecorder
4749
import io.element.android.tests.testutils.lambda.value
4850
import io.element.android.tests.testutils.testCoroutineDispatchers
4951
import kotlinx.coroutines.ExperimentalCoroutinesApi
5052
import kotlinx.coroutines.cancelAndJoin
53+
import kotlinx.coroutines.flow.MutableStateFlow
5154
import kotlinx.coroutines.launch
5255
import kotlinx.coroutines.sync.Mutex
5356
import kotlinx.coroutines.test.TestScope
@@ -86,8 +89,9 @@ class CallScreenPresenterTest {
8689
@Test
8790
fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
8891
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
92+
val syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
8993
val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
90-
val client = FakeMatrixClient().apply {
94+
val client = FakeMatrixClient(syncService = syncService).apply {
9195
givenGetRoomResult(A_ROOM_ID, fakeRoom)
9296
}
9397
val widgetDriver = FakeMatrixWidgetDriver()
@@ -216,7 +220,12 @@ class CallScreenPresenterTest {
216220
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
217221
val navigator = FakeCallScreenNavigator()
218222
val widgetDriver = FakeMatrixWidgetDriver()
219-
val matrixClient = FakeMatrixClient()
223+
val syncStateFlow = MutableStateFlow(SyncState.Idle)
224+
val startSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
225+
val syncService = FakeSyncService(syncStateFlow = syncStateFlow).apply {
226+
this.startSyncLambda = startSyncLambda
227+
}
228+
val matrixClient = FakeMatrixClient(syncService = syncService)
220229
val presenter = createCallScreenPresenter(
221230
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
222231
widgetDriver = widgetDriver,
@@ -230,7 +239,7 @@ class CallScreenPresenterTest {
230239
}.test {
231240
consumeItemsUntilTimeout()
232241

233-
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running)
242+
assert(startSyncLambda).isCalledOnce()
234243

235244
cancelAndIgnoreRemainingEvents()
236245
}
@@ -240,7 +249,12 @@ class CallScreenPresenterTest {
240249
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
241250
val navigator = FakeCallScreenNavigator()
242251
val widgetDriver = FakeMatrixWidgetDriver()
243-
val matrixClient = FakeMatrixClient()
252+
val syncStateFlow = MutableStateFlow(SyncState.Running)
253+
val stopSyncLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
254+
val syncService = FakeSyncService(syncStateFlow = syncStateFlow).apply {
255+
this.stopSyncLambda = stopSyncLambda
256+
}
257+
val matrixClient = FakeMatrixClient(syncService = syncService)
244258
val presenter = createCallScreenPresenter(
245259
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
246260
widgetDriver = widgetDriver,
@@ -262,7 +276,7 @@ class CallScreenPresenterTest {
262276

263277
job.cancelAndJoin()
264278

265-
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Terminated)
279+
assert(stopSyncLambda).isCalledOnce()
266280
}
267281

268282
private fun TestScope.createCallScreenPresenter(

features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
9595
import kotlinx.coroutines.CoroutineScope
9696
import kotlinx.coroutines.SupervisorJob
9797
import kotlinx.coroutines.cancel
98+
import kotlinx.coroutines.flow.MutableStateFlow
9899
import kotlinx.coroutines.test.TestScope
99100
import kotlinx.coroutines.test.runTest
100101
import org.junit.Rule
@@ -219,7 +220,7 @@ class RoomListPresenterTest {
219220
val encryptionService = FakeEncryptionService().apply {
220221
emitRecoveryState(RecoveryState.INCOMPLETE)
221222
}
222-
val syncService = FakeSyncService(initialState = SyncState.Running)
223+
val syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
223224
val presenter = createRoomListPresenter(
224225
client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService),
225226
coroutineScope = scope,
@@ -250,7 +251,7 @@ class RoomListPresenterTest {
250251
sessionVerificationService = FakeSessionVerificationService().apply {
251252
givenNeedsSessionVerification(false)
252253
},
253-
syncService = FakeSyncService(initialState = SyncState.Running)
254+
syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
254255
)
255256
val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
256257
val presenter = createRoomListPresenter(

libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ class FakeMatrixRoom(
139139
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
140140
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
141141
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
142+
private val subscribeToSyncLambda: () -> Unit = { lambdaError() },
142143
) : MatrixRoom {
143144
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
144145
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
@@ -181,7 +182,9 @@ class FakeMatrixRoom(
181182
timelineFocusedOnEventResult(eventId)
182183
}
183184

184-
override suspend fun subscribeToSync() = Unit
185+
override suspend fun subscribeToSync() {
186+
subscribeToSyncLambda()
187+
}
185188

186189
override suspend fun powerLevels(): Result<MatrixRoomPowerLevels> {
187190
return powerLevelsResult()

libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
2222
import kotlinx.coroutines.flow.StateFlow
2323

2424
class FakeSyncService(
25-
initialState: SyncState = SyncState.Idle
25+
syncStateFlow: MutableStateFlow<SyncState> = MutableStateFlow(SyncState.Idle)
2626
) : SyncService {
27-
private val syncStateFlow = MutableStateFlow(initialState)
28-
29-
fun simulateError() {
30-
syncStateFlow.value = SyncState.Error
31-
}
32-
27+
var startSyncLambda: () -> Result<Unit> = { Result.success(Unit) }
3328
override suspend fun startSync(): Result<Unit> {
34-
syncStateFlow.value = SyncState.Running
35-
return Result.success(Unit)
29+
return startSyncLambda()
3630
}
3731

32+
var stopSyncLambda: () -> Result<Unit> = { Result.success(Unit) }
3833
override suspend fun stopSync(): Result<Unit> {
39-
syncStateFlow.value = SyncState.Terminated
40-
return Result.success(Unit)
34+
return stopSyncLambda()
4135
}
4236

4337
override val syncState: StateFlow<SyncState> = syncStateFlow

libraries/push/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,6 @@ dependencies {
8282
testImplementation(projects.services.appnavstate.test)
8383
testImplementation(projects.services.toolbox.impl)
8484
testImplementation(projects.services.toolbox.test)
85+
testImplementation(projects.libraries.featureflag.test)
86+
testImplementation(libs.kotlinx.collections.immutable)
8587
}

libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ package io.element.android.libraries.push.impl.push
1818

1919
import com.squareup.anvil.annotations.ContributesBinding
2020
import io.element.android.libraries.di.AppScope
21-
import io.element.android.libraries.featureflag.api.FeatureFlagService
22-
import io.element.android.libraries.featureflag.api.FeatureFlags
23-
import io.element.android.libraries.matrix.api.MatrixClientProvider
2421
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
2522
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
2623
import kotlinx.coroutines.CoroutineScope
@@ -35,23 +32,12 @@ interface OnNotifiableEventReceived {
3532
class DefaultOnNotifiableEventReceived @Inject constructor(
3633
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
3734
private val coroutineScope: CoroutineScope,
38-
private val matrixClientProvider: MatrixClientProvider,
39-
private val featureFlagService: FeatureFlagService,
35+
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
4036
) : OnNotifiableEventReceived {
4137
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
4238
coroutineScope.launch {
43-
subscribeToRoomIfNeeded(notifiableEvent)
39+
launch { syncOnNotifiableEvent(notifiableEvent) }
4440
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
4541
}
4642
}
47-
48-
private fun CoroutineScope.subscribeToRoomIfNeeded(notifiableEvent: NotifiableEvent) = launch {
49-
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) {
50-
return@launch
51-
}
52-
val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@launch
53-
client.getRoom(notifiableEvent.roomId)?.use { room ->
54-
room.subscribeToSync()
55-
}
56-
}
5743
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.push.impl.push
18+
19+
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
20+
import io.element.android.libraries.featureflag.api.FeatureFlagService
21+
import io.element.android.libraries.featureflag.api.FeatureFlags
22+
import io.element.android.libraries.matrix.api.MatrixClientProvider
23+
import io.element.android.libraries.matrix.api.core.EventId
24+
import io.element.android.libraries.matrix.api.room.MatrixRoom
25+
import io.element.android.libraries.matrix.api.sync.SyncService
26+
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
27+
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
28+
import io.element.android.services.appnavstate.api.AppForegroundStateService
29+
import kotlinx.coroutines.flow.first
30+
import kotlinx.coroutines.withContext
31+
import kotlinx.coroutines.withTimeoutOrNull
32+
import java.util.concurrent.atomic.AtomicInteger
33+
import javax.inject.Inject
34+
import kotlin.time.Duration
35+
import kotlin.time.Duration.Companion.seconds
36+
37+
class SyncOnNotifiableEvent @Inject constructor(
38+
private val matrixClientProvider: MatrixClientProvider,
39+
private val featureFlagService: FeatureFlagService,
40+
private val appForegroundStateService: AppForegroundStateService,
41+
private val dispatchers: CoroutineDispatchers,
42+
) {
43+
private var syncCounter = AtomicInteger(0)
44+
45+
suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) {
46+
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) {
47+
return@withContext
48+
}
49+
val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext
50+
client.getRoom(notifiableEvent.roomId)?.use { room ->
51+
room.subscribeToSync()
52+
53+
// If the app is in foreground, sync is already running, so just add the subscription.
54+
if (!appForegroundStateService.isInForeground.value) {
55+
val syncService = client.syncService()
56+
syncService.startSyncIfNeeded()
57+
room.waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds)
58+
syncService.stopSyncIfNeeded()
59+
}
60+
}
61+
}
62+
63+
private suspend fun MatrixRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) {
64+
withTimeoutOrNull(timeout) {
65+
liveTimeline.timelineItems.first { timelineItems ->
66+
timelineItems.any { timelineItem ->
67+
when (timelineItem) {
68+
is MatrixTimelineItem.Event -> timelineItem.eventId == eventId
69+
else -> false
70+
}
71+
}
72+
}
73+
}
74+
}
75+
76+
private suspend fun SyncService.startSyncIfNeeded() {
77+
if (syncCounter.getAndIncrement() == 0) {
78+
startSync()
79+
}
80+
}
81+
82+
private suspend fun SyncService.stopSyncIfNeeded() {
83+
if (syncCounter.decrementAndGet() == 0 && !appForegroundStateService.isInForeground.value) {
84+
stopSync()
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)