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.
This commit is contained in:
Benoit Marty 2024-11-22 18:11:14 +01:00
parent 126181a8b2
commit 6fc572d48e
6 changed files with 61 additions and 10 deletions

View file

@ -7,7 +7,7 @@ import extension.setupAnvil
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
} }

View file

@ -93,13 +93,16 @@ class DefaultPushHandler @Inject constructor(
when (resolvedPushEvent) { when (resolvedPushEvent) {
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data") null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
is ResolvedPushEvent.Event -> { is ResolvedPushEvent.Event -> {
when (resolvedPushEvent.notifiableEvent) { when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
is NotifiableRingingCallEvent -> handleRingingCallEvent(resolvedPushEvent.notifiableEvent) is NotifiableRingingCallEvent -> {
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
handleRingingCallEvent(notifiableEvent)
}
else -> { else -> {
val userPushStore = userPushStoreFactory.getOrCreate(userId) val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first() val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
if (areNotificationsEnabled) { if (areNotificationsEnabled) {
onNotifiableEventReceived.onNotifiableEventReceived(resolvedPushEvent.notifiableEvent) onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
} else { } else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
} }

View file

@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager 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.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -28,7 +29,9 @@ class DefaultOnNotifiableEventReceived @Inject constructor(
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
coroutineScope.launch { coroutineScope.launch {
launch { syncOnNotifiableEvent(notifiableEvent) } launch { syncOnNotifiableEvent(notifiableEvent) }
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent) if (notifiableEvent !is NotifiableRingingCallEvent) {
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
}
} }
} }
} }

View file

@ -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.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem 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.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.appnavstate.api.AppForegroundStateService
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -34,7 +35,8 @@ class SyncOnNotifiableEvent @Inject constructor(
private var syncCounter = AtomicInteger(0) private var syncCounter = AtomicInteger(0)
suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) { 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 return@withContext
} }
val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext val client = matrixClientProvider.getOrRestore(notifiableEvent.sessionId).getOrNull() ?: return@withContext
@ -45,12 +47,28 @@ class SyncOnNotifiableEvent @Inject constructor(
if (!appForegroundStateService.isInForeground.value) { if (!appForegroundStateService.isInForeground.value) {
val syncService = client.syncService() val syncService = client.syncService()
syncService.startSyncIfNeeded() 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() 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) { private suspend fun MatrixRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) {
withTimeoutOrNull(timeout) { withTimeoutOrNull(timeout) {
liveTimeline.timelineItems.first { timelineItems -> liveTimeline.timelineItems.first { timelineItems ->

View file

@ -240,6 +240,7 @@ class DefaultPushHandlerTest {
) )
val handleIncomingCallLambda = lambdaRecorder<CallType.RoomCall, EventId, UserId, String?, String?, String?, String, Unit> { _, _, _, _, _, _, _ -> } val handleIncomingCallLambda = lambdaRecorder<CallType.RoomCall, EventId, UserId, String?, String?, String?, String, Unit> { _, _, _, _, _, _, _ -> }
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint, elementCallEntryPoint = elementCallEntryPoint,
notifiableEventResult = { _, _, _ -> notifiableEventResult = { _, _, _ ->
@ -249,10 +250,11 @@ class DefaultPushHandlerTest {
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID } getUserIdFromSecretResult = { A_USER_ID }
), ),
onNotifiableEventReceived = onNotifiableEventReceived,
) )
defaultPushHandler.handle(aPushData) defaultPushHandler.handle(aPushData)
handleIncomingCallLambda.assertions().isCalledOnce() handleIncomingCallLambda.assertions().isCalledOnce()
onNotifiableEventReceived.assertions().isCalledOnce()
} }
@Test @Test
@ -310,7 +312,7 @@ class DefaultPushHandlerTest {
) )
defaultPushHandler.handle(aPushData) defaultPushHandler.handle(aPushData)
handleIncomingCallLambda.assertions().isCalledOnce() handleIncomingCallLambda.assertions().isCalledOnce()
onNotifiableEventReceived.assertions().isNeverCalled() onNotifiableEventReceived.assertions().isCalledOnce()
} }
@Test @Test

View file

@ -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.sync.FakeSyncService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem 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.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
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
@ -60,6 +61,7 @@ class SyncOnNotifiableEventTest {
} }
private val notifiableEvent = aNotifiableMessageEvent() private val notifiableEvent = aNotifiableMessageEvent()
private val incomingCallNotifiableEvent = aNotifiableCallEvent()
@Test @Test
fun `when feature flag is disabled, nothing happens`() = runTest { fun `when feature flag is disabled, nothing happens`() = runTest {
@ -72,15 +74,38 @@ class SyncOnNotifiableEventTest {
assert(subscribeToSyncLambda).isNeverCalled() 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 @Test
fun `when feature flag is enabled and app is in foreground, sync is not started`() = runTest { 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) val sut = createSyncOnNotifiableEvent(client = client, isAppInForeground = true, isSyncOnPushEnabled = true)
sut(notifiableEvent) sut(notifiableEvent)
sut(incomingCallNotifiableEvent)
assert(startSyncLambda).isNeverCalled() assert(startSyncLambda).isNeverCalled()
assert(stopSyncLambda).isNeverCalled() assert(stopSyncLambda).isNeverCalled()
assert(subscribeToSyncLambda).isCalledOnce() assert(subscribeToSyncLambda).isCalledExactly(2)
} }
@Test @Test