Keep call notification ringing while a call is present in the room (#4634)

This commit is contained in:
Jorge Martin Espinosa 2025-05-09 11:38:43 +02:00 committed by GitHub
parent 3391e7cc55
commit c39b480f82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 217 additions and 99 deletions

View file

@ -9,16 +9,22 @@ package io.element.android.libraries.push.impl.notifications
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.push.impl.R
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.toolbox.api.strings.StringProvider
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
/**
* Helper to resolve a valid [NotifiableEvent] from a [NotificationData].
@ -31,7 +37,7 @@ interface CallNotificationEventResolver {
* @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`.
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
*/
fun resolveEvent(
suspend fun resolveEvent(
sessionId: SessionId,
notificationData: NotificationData,
forceNotify: Boolean = false,
@ -41,8 +47,10 @@ interface CallNotificationEventResolver {
@ContributesBinding(AppScope::class)
class DefaultCallNotificationEventResolver @Inject constructor(
private val stringProvider: StringProvider,
private val appForegroundStateService: AppForegroundStateService,
private val clientProvider: MatrixClientProvider,
) : CallNotificationEventResolver {
override fun resolveEvent(
override suspend fun resolveEvent(
sessionId: SessionId,
notificationData: NotificationData,
forceNotify: Boolean
@ -50,8 +58,32 @@ class DefaultCallNotificationEventResolver @Inject constructor(
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
?: throw ResolvingException("content is not a call notify")
val previousRingingCallStatus = appForegroundStateService.hasRingingCall.value
// We need the sync service working to get the updated room info
val isRoomCallActive = runCatching {
if (content.type == CallNotifyType.RING) {
appForegroundStateService.updateHasRingingCall(true)
val client = clientProvider.getOrRestore(sessionId).getOrNull() ?: throw ResolvingException("Session $sessionId not found")
val room = client.getRoom(notificationData.roomId) ?: throw ResolvingException("Room ${notificationData.roomId} not found")
// Give a few seconds for the room info flow to catch up with the sync, if needed - this is usually instant
val isActive = withTimeoutOrNull(3.seconds) { room.roomInfoFlow.firstOrNull { it.hasRoomCall } }?.hasRoomCall ?: false
// We no longer need the sync service to be active because of a call notification.
appForegroundStateService.updateHasRingingCall(previousRingingCallStatus)
isActive
} else {
// If the call notification is not of ringing type, we don't need to check if the call is active
false
}
}.onFailure {
// Make sure to reset the hasRingingCall state in case of failure
appForegroundStateService.updateHasRingingCall(previousRingingCallStatus)
}.getOrDefault(false)
notificationData.run {
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
if (content.type == CallNotifyType.RING && isRoomCallActive && !forceNotify) {
NotifiableRingingCallEvent(
sessionId = sessionId,
roomId = roomId,
@ -70,9 +102,7 @@ class DefaultCallNotificationEventResolver @Inject constructor(
senderAvatarUrl = senderAvatarUrl,
)
} else {
val now = System.currentTimeMillis()
val elapsed = now - timestamp
Timber.d("Event $eventId is call notify but should not ring: $timestamp vs $now ($elapsed ms elapsed), notify: ${content.type}")
Timber.d("Event $eventId is call notify but should not ring: $isRoomCallActive, notify: ${content.type}")
// Create a simple message notification event
buildNotifiableMessageEvent(
sessionId = sessionId,

View file

@ -12,8 +12,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import java.time.Instant
import kotlin.time.Duration.Companion.seconds
data class NotifiableRingingCallEvent(
override val sessionId: SessionId,
@ -31,13 +29,4 @@ data class NotifiableRingingCallEvent(
val roomAvatarUrl: String? = null,
val callNotifyType: CallNotifyType,
val timestamp: Long,
) : NotifiableEvent {
companion object {
fun shouldRing(callNotifyType: CallNotifyType, timestamp: Long): Boolean {
val timeout = 10.seconds.inWholeMilliseconds
val elapsed = Instant.now().toEpochMilli() - timestamp
// Only ring if the type is RING and the elapsed time is less than the timeout
return callNotifyType == CallNotifyType.RING && elapsed < timeout
}
}
}
) : NotifiableEvent