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