Notify of ringing call when there's an active call (#3003)
* Add `CallNotificationEventResolver` to be able to force the new ringing notification to be non-ringing given an existing ringing one.
This commit is contained in:
parent
02d6fa7a92
commit
f07ec61ecc
12 changed files with 232 additions and 81 deletions
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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.core.SessionId
|
||||
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.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Helper to resolve a valid [NotifiableEvent] from a [NotificationData].
|
||||
*/
|
||||
interface CallNotificationEventResolver {
|
||||
/**
|
||||
* Resolve a call notification event from a notification data depending on whether it should be a ringing one or not.
|
||||
* @param sessionId the current session id
|
||||
* @param notificationData the notification data
|
||||
* @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(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean = false): NotifiableEvent?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallNotificationEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
) : CallNotificationEventResolver {
|
||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify ?: return null
|
||||
return notificationData.run {
|
||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
roomName = roomDisplayName,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
timestamp = this.timestamp,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
description = stringProvider.getString(R.string.notification_incoming_call),
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
callNotifyType = content.type,
|
||||
senderId = content.senderId,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
)
|
||||
} else {
|
||||
// Create a simple message notification event
|
||||
buildNotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
noisy = true,
|
||||
timestamp = this.timestamp,
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = "☎️ ${stringProvider.getString(R.string.notification_incoming_call)}",
|
||||
roomName = roomDisplayName,
|
||||
roomIsDirect = isDirect,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
|
|||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
|
@ -79,6 +78,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
||||
// Restore session
|
||||
|
|
@ -174,42 +174,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
)
|
||||
}
|
||||
is NotificationContent.MessageLike.CallNotify -> {
|
||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp)) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
roomName = roomDisplayName,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
timestamp = this.timestamp,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
description = stringProvider.getString(R.string.notification_incoming_call),
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
callNotifyType = content.type,
|
||||
senderId = content.senderId,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
)
|
||||
} else {
|
||||
// Create a simple message notification event
|
||||
buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
noisy = true,
|
||||
timestamp = this.timestamp,
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = "☎️ ${stringProvider.getString(R.string.notification_incoming_call)}",
|
||||
roomName = roomDisplayName,
|
||||
roomIsDirect = isDirect,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
}
|
||||
callNotificationEventResolver.resolveEvent(userId, this)
|
||||
}
|
||||
NotificationContent.MessageLike.KeyVerificationAccept,
|
||||
NotificationContent.MessageLike.KeyVerificationCancel,
|
||||
|
|
@ -349,7 +314,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun buildNotifiableMessageEvent(
|
||||
internal fun buildNotifiableMessageEvent(
|
||||
sessionId: SessionId,
|
||||
senderId: UserId,
|
||||
roomId: RoomId,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
|
@ -26,8 +27,9 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
|
||||
private val notifiableEventResolver: NotifiableEventResolver,
|
||||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
) : OnMissedCallNotificationHandler {
|
||||
override suspend fun addMissedCallNotification(
|
||||
sessionId: SessionId,
|
||||
|
|
@ -35,7 +37,18 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
|||
eventId: EventId,
|
||||
) {
|
||||
// Resolve the event and add a notification for it, at this point it should no longer be a ringing one
|
||||
val notifiableEvent = notifiableEventResolver.resolveEvent(sessionId, roomId, eventId)
|
||||
val notificationData = matrixClientProvider.getOrRestore(sessionId).getOrNull()
|
||||
?.notificationService()
|
||||
?.getNotification(sessionId, roomId, eventId)
|
||||
?.getOrNull()
|
||||
?: return
|
||||
|
||||
val notifiableEvent = callNotificationEventResolver.resolveEvent(
|
||||
sessionId = sessionId,
|
||||
notificationData = notificationData,
|
||||
// Make sure the notifiable event is not a ringing one
|
||||
forceNotify = true,
|
||||
)
|
||||
notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -660,6 +660,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
notificationMediaRepoFactory = notificationMediaRepoFactory,
|
||||
context = context,
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
callNotificationEventResolver = DefaultCallNotificationEventResolver(
|
||||
stringProvider = AndroidStringProvider(context.resources)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,16 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
||||
import io.element.android.libraries.matrix.test.notification.aNotificationData
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
|
||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -47,7 +52,15 @@ class DefaultOnMissedCallNotificationHandlerTest {
|
|||
val dataFactory = FakeNotificationDataFactory(
|
||||
messageEventToNotificationsResult = lambdaRecorder { _, _, _ -> emptyList() }
|
||||
)
|
||||
// Create a fake matrix client provider that returns a fake matrix client with a fake notification service that returns a valid notification data
|
||||
val matrixClientProvider = FakeMatrixClientProvider(getClient = {
|
||||
val notificationService = FakeNotificationService().apply {
|
||||
givenGetNotificationResult(Result.success(aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false)))
|
||||
}
|
||||
Result.success(FakeMatrixClient(notificationService = notificationService))
|
||||
})
|
||||
val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler(
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
defaultNotificationDrawerManager = DefaultNotificationDrawerManager(
|
||||
notificationManager = mockk(relaxed = true),
|
||||
notificationRenderer = NotificationRenderer(
|
||||
|
|
@ -60,7 +73,7 @@ class DefaultOnMissedCallNotificationHandlerTest {
|
|||
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||
),
|
||||
notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent() }),
|
||||
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ -> aNotifiableMessageEvent() }),
|
||||
)
|
||||
|
||||
defaultOnMissedCallNotificationHandler.addMissedCallNotification(
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ fun aNotifiableMessageEvent(
|
|||
type = type,
|
||||
)
|
||||
|
||||
fun anNotifiableCallEvent(
|
||||
fun aNotifiableCallEvent(
|
||||
sessionId: SessionId = A_SESSION_ID,
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
eventId: EventId = AN_EVENT_ID,
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer
|
|||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
|
||||
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.anNotifiableCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||
|
|
@ -240,7 +240,7 @@ class DefaultPushHandlerTest {
|
|||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notifiableEventResult = { _, _, _ -> anNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) },
|
||||
notifiableEventResult = { _, _, _ -> aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) },
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue