From 6fc572d48e59d3eee04a8a53dd128968bdb6759c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Nov 2024 18:11:14 +0100 Subject: [PATCH] Ensure that the SDK is syncing during an incoming call so that the application can detect if the call has been answered on another session. This is dealing with the case the application is not in foreground. --- libraries/push/impl/build.gradle.kts | 2 +- .../push/impl/push/DefaultPushHandler.kt | 9 ++++--- .../impl/push/OnNotifiableEventReceived.kt | 5 +++- .../push/impl/push/SyncOnNotifiableEvent.kt | 22 +++++++++++++-- .../push/impl/push/DefaultPushHandlerTest.kt | 6 +++-- .../impl/push/SyncOnNotifiableEventTest.kt | 27 ++++++++++++++++++- 6 files changed, 61 insertions(+), 10 deletions(-) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 6489f26dec..6692f5da27 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -7,7 +7,7 @@ import extension.setupAnvil * Please see LICENSE in the repository root for full details. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") alias(libs.plugins.kotlin.serialization) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 3fd772a411..9dfbed3f80 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -93,13 +93,16 @@ class DefaultPushHandler @Inject constructor( when (resolvedPushEvent) { null -> Timber.tag(loggerTag.value).w("Unable to get a notification data") is ResolvedPushEvent.Event -> { - when (resolvedPushEvent.notifiableEvent) { - is NotifiableRingingCallEvent -> handleRingingCallEvent(resolvedPushEvent.notifiableEvent) + when (val notifiableEvent = resolvedPushEvent.notifiableEvent) { + is NotifiableRingingCallEvent -> { + onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) + handleRingingCallEvent(notifiableEvent) + } else -> { val userPushStore = userPushStoreFactory.getOrCreate(userId) val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first() if (areNotificationsEnabled) { - onNotifiableEventReceived.onNotifiableEventReceived(resolvedPushEvent.notifiableEvent) + onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) } else { Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt index 0857899180..346ae89f10 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -28,7 +29,9 @@ class DefaultOnNotifiableEventReceived @Inject constructor( override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { coroutineScope.launch { launch { syncOnNotifiableEvent(notifiableEvent) } - defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent) + if (notifiableEvent !is NotifiableRingingCallEvent) { + defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent) + } } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt index a12dc7d6c4..0f43efaa31 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEvent.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.sync.SyncService 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.NotifiableRingingCallEvent import io.element.android.services.appnavstate.api.AppForegroundStateService import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext @@ -34,7 +35,8 @@ class SyncOnNotifiableEvent @Inject constructor( private var syncCounter = AtomicInteger(0) suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) { - if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) { + val isRingingCallEvent = notifiableEvent is NotifiableRingingCallEvent + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush) && !isRingingCallEvent) { return@withContext } val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext @@ -45,12 +47,28 @@ class SyncOnNotifiableEvent @Inject constructor( if (!appForegroundStateService.isInForeground.value) { val syncService = client.syncService() syncService.startSyncIfNeeded() - room.waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds) + if (isRingingCallEvent) { + room.waitsUntilUserIsInTheCall(timeout = 60.seconds) + } else { + room.waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds) + } syncService.stopSyncIfNeeded() } } } + /** + * User can be in the call if they answer using another session. + * If the user does not join the call, the timeout will be reached. + */ + private suspend fun MatrixRoom.waitsUntilUserIsInTheCall(timeout: Duration) { + withTimeoutOrNull(timeout) { + roomInfoFlow.first { + sessionId in it.activeRoomCallParticipants + } + } + } + private suspend fun MatrixRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) { withTimeoutOrNull(timeout) { liveTimeline.timelineItems.first { timelineItems -> diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 1968655e86..56d5ab2868 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -240,6 +240,7 @@ class DefaultPushHandlerTest { ) val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> } val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) + val onNotifiableEventReceived = lambdaRecorder {} val defaultPushHandler = createDefaultPushHandler( elementCallEntryPoint = elementCallEntryPoint, notifiableEventResult = { _, _, _ -> @@ -249,10 +250,11 @@ class DefaultPushHandlerTest { pushClientSecret = FakePushClientSecret( getUserIdFromSecretResult = { A_USER_ID } ), + onNotifiableEventReceived = onNotifiableEventReceived, ) defaultPushHandler.handle(aPushData) - handleIncomingCallLambda.assertions().isCalledOnce() + onNotifiableEventReceived.assertions().isCalledOnce() } @Test @@ -310,7 +312,7 @@ class DefaultPushHandlerTest { ) defaultPushHandler.handle(aPushData) handleIncomingCallLambda.assertions().isCalledOnce() - onNotifiableEventReceived.assertions().isNeverCalled() + onNotifiableEventReceived.assertions().isCalledOnce() } @Test diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt index 533d7bcb7d..5f63064b29 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/SyncOnNotifiableEventTest.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom 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.aNotifiableMessageEvent import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.tests.testutils.lambda.assert @@ -60,6 +61,7 @@ class SyncOnNotifiableEventTest { } private val notifiableEvent = aNotifiableMessageEvent() + private val incomingCallNotifiableEvent = aNotifiableCallEvent() @Test fun `when feature flag is disabled, nothing happens`() = runTest { @@ -72,15 +74,38 @@ class SyncOnNotifiableEventTest { assert(subscribeToSyncLambda).isNeverCalled() } + @Test + fun `when feature flag is enabled, a ringing call starts and stops the sync`() = runTest { + val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = false, isSyncOnPushEnabled = true) + + sut(incomingCallNotifiableEvent) + + assert(startSyncLambda).isCalledOnce() + assert(stopSyncLambda).isCalledOnce() + assert(subscribeToSyncLambda).isCalledOnce() + } + + @Test + fun `when feature flag is disabled, a ringing call starts and stops the sync`() = runTest { + val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = false, isSyncOnPushEnabled = false) + + sut(incomingCallNotifiableEvent) + + assert(startSyncLambda).isCalledOnce() + assert(stopSyncLambda).isCalledOnce() + assert(subscribeToSyncLambda).isCalledOnce() + } + @Test fun `when feature flag is enabled and app is in foreground, sync is not started`() = runTest { val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = true, isSyncOnPushEnabled = true) sut(notifiableEvent) + sut(incomingCallNotifiableEvent) assert(startSyncLambda).isNeverCalled() assert(stopSyncLambda).isNeverCalled() - assert(subscribeToSyncLambda).isCalledOnce() + assert(subscribeToSyncLambda).isCalledExactly(2) } @Test