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.
*/
plugins {
id("io.element.android-library")
id("io.element.android-compose-library")
alias(libs.plugins.kotlin.serialization)
}

View File

@@ -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.")
}

View File

@@ -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)
}
}
}
}

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.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 ->

View File

@@ -240,6 +240,7 @@ class DefaultPushHandlerTest {
)
val handleIncomingCallLambda = lambdaRecorder<CallType.RoomCall, EventId, UserId, String?, String?, String?, String, Unit> { _, _, _, _, _, _, _ -> }
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
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

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.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