Merge pull request #3320 from element-hq/feature/bma/cleanNotificationOfRedactedEvent

Redact message on displayed notification
This commit is contained in:
Benoit Marty
2024-08-29 14:46:13 +02:00
committed by GitHub
19 changed files with 644 additions and 260 deletions

View File

@@ -79,8 +79,8 @@ sealed interface NotificationContent {
) : MessageLike
data class RoomRedaction(
val redactedEventId: String?,
val reason: String?
val redactedEventId: EventId?,
val reason: String?,
) : MessageLike
data object Sticker : MessageLike

View File

@@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent
@@ -94,7 +95,10 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
is MessageLikeEventContent.RoomMessage -> {
NotificationContent.MessageLike.RoomMessage(senderId, EventMessageMapper().mapMessageType(messageType))
}
is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction(redactedEventId = redactedEventId, reason = reason)
is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction(
redactedEventId = redactedEventId?.let(::EventId),
reason = reason,
)
MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(senderId, question)
}

View File

@@ -63,6 +63,8 @@ const val A_MESSAGE = "Hello world!"
const val A_REPLY = "OK, I'll be there!"
const val ANOTHER_MESSAGE = "Hello universe!"
const val A_REDACTION_REASON = "A redaction reason"
const val A_HOMESERVER_URL = "matrix.org"
const val A_HOMESERVER_URL_2 = "matrix-client.org"

View File

@@ -26,7 +26,6 @@ import io.element.android.libraries.push.api.notifications.NotificationIdProvide
import javax.inject.Inject
interface ActiveNotificationsProvider {
fun getAllNotifications(): List<StatusBarNotification>
fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification>
fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification>
@@ -39,10 +38,6 @@ interface ActiveNotificationsProvider {
class DefaultActiveNotificationsProvider @Inject constructor(
private val notificationManager: NotificationManagerCompat,
) : ActiveNotificationsProvider {
override fun getAllNotifications(): List<StatusBarNotification> {
return notificationManager.activeNotifications
}
override fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification> {
return notificationManager.activeNotifications.filter { it.notification.group == sessionId.value }
}

View File

@@ -50,8 +50,8 @@ import io.element.android.libraries.matrix.ui.messages.toPlainText
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
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.ResolvedPushEvent
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
@@ -67,7 +67,7 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
*/
interface NotifiableEventResolver {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent?
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent?
}
@ContributesBinding(AppScope::class)
@@ -80,7 +80,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
private val permalinkParser: PermalinkParser,
private val callNotificationEventResolver: CallNotificationEventResolver,
) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
// Restore session
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val notificationService = client.notificationService()
@@ -99,8 +99,9 @@ class DefaultNotifiableEventResolver @Inject constructor(
private suspend fun NotificationData.asNotifiableEvent(
client: MatrixClient,
userId: SessionId,
): NotifiableEvent? {
return when (val content = this.content) {
): ResolvedPushEvent? {
val content = this.content
val notifiableEvent = when (content) {
is NotificationContent.MessageLike.RoomMessage -> {
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
@@ -204,8 +205,9 @@ class DefaultNotifiableEventResolver @Inject constructor(
NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also {
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
}
is NotificationContent.MessageLike.RoomRedaction -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for redaction")
is NotificationContent.MessageLike.RoomRedaction -> {
// Note: this case will be handled below
null
}
NotificationContent.MessageLike.Sticker -> null.also {
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
@@ -233,6 +235,25 @@ class DefaultNotifiableEventResolver @Inject constructor(
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
}
}
return if (notifiableEvent != null) {
ResolvedPushEvent.Event(notifiableEvent)
} else if (content is NotificationContent.MessageLike.RoomRedaction) {
val redactedEventId = content.redactedEventId
if (redactedEventId == null) {
Timber.tag(loggerTag.value).d("redactedEventId is null.")
null
} else {
ResolvedPushEvent.Redaction(
sessionId = userId,
roomId = roomId,
redactedEventId = redactedEventId,
reason = content.reason,
)
}
} else {
null
}
}
private fun fallbackNotifiableEvent(

View File

@@ -42,77 +42,75 @@ class NotificationRenderer @Inject constructor(
imageLoader: ImageLoader,
) {
val groupedEvents = eventsToProcess.groupByType()
with(notificationDataFactory) {
val roomNotifications = toNotifications(groupedEvents.roomEvents, currentUser, imageLoader)
val invitationNotifications = toNotifications(groupedEvents.invitationEvents)
val simpleNotifications = toNotifications(groupedEvents.simpleEvents)
val fallbackNotifications = toNotifications(groupedEvents.fallbackEvents)
val summaryNotification = createSummaryNotification(
currentUser = currentUser,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications,
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, currentUser, imageLoader)
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents)
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents)
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents)
val summaryNotification = notificationDataFactory.createSummaryNotification(
currentUser = currentUser,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications,
)
// Remove summary first to avoid briefly displaying it after dismissing the last notification
if (summaryNotification == SummaryNotification.Removed) {
Timber.tag(loggerTag.value).d("Removing summary notification")
notificationDisplayer.cancelNotificationMessage(
tag = null,
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
)
}
// Remove summary first to avoid briefly displaying it after dismissing the last notification
if (summaryNotification == SummaryNotification.Removed) {
Timber.tag(loggerTag.value).d("Removing summary notification")
notificationDisplayer.cancelNotificationMessage(
tag = null,
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
)
}
roomNotifications.forEach { notificationData ->
notificationDisplayer.showNotificationMessage(
tag = notificationData.roomId.value,
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
roomNotifications.forEach { notificationData ->
invitationNotifications.forEach { notificationData ->
if (useCompleteNotificationFormat) {
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
notificationDisplayer.showNotificationMessage(
tag = notificationData.roomId.value,
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
tag = notificationData.key,
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
}
invitationNotifications.forEach { notificationData ->
if (useCompleteNotificationFormat) {
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
notificationDisplayer.showNotificationMessage(
tag = notificationData.key,
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
}
simpleNotifications.forEach { notificationData ->
if (useCompleteNotificationFormat) {
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
notificationDisplayer.showNotificationMessage(
tag = notificationData.key,
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
}
// Show only the first fallback notification
if (fallbackNotifications.isNotEmpty()) {
Timber.tag(loggerTag.value).d("Showing fallback notification")
simpleNotifications.forEach { notificationData ->
if (useCompleteNotificationFormat) {
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
notificationDisplayer.showNotificationMessage(
tag = "FALLBACK",
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
notification = fallbackNotifications.first().notification
tag = notificationData.key,
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
}
// Update summary last to avoid briefly displaying it before other notifications
if (summaryNotification is SummaryNotification.Update) {
Timber.tag(loggerTag.value).d("Updating summary notification")
notificationDisplayer.showNotificationMessage(
tag = null,
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
notification = summaryNotification.notification
)
}
// Show only the first fallback notification
if (fallbackNotifications.isNotEmpty()) {
Timber.tag(loggerTag.value).d("Showing fallback notification")
notificationDisplayer.showNotificationMessage(
tag = "FALLBACK",
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
notification = fallbackNotifications.first().notification
)
}
// Update summary last to avoid briefly displaying it before other notifications
if (summaryNotification is SummaryNotification.Update) {
Timber.tag(loggerTag.value).d("Updating summary notification")
notificationDisplayer.showNotificationMessage(
tag = null,
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
notification = summaryNotification.notification
)
}
}
}

View File

@@ -430,6 +430,7 @@ class DefaultNotificationCreator @Inject constructor(
event.imageUri?.let {
message.setData("image/", it)
}
message.extras.putString(MESSAGE_EVENT_ID, event.eventId.value)
}
addMessage(message)
}
@@ -446,10 +447,10 @@ class DefaultNotificationCreator @Inject constructor(
): MessagingStyle {
return MessagingStyle(
Person.Builder()
.setName(user.displayName?.annotateForDebug(50))
.setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader))
.setKey(sessionId.value)
.build()
.setName(user.displayName?.annotateForDebug(50))
.setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader))
.setKey(sessionId.value)
.build()
).also {
it.conversationTitle = roomName.takeIf { roomIsGroup }
it.isGroupConversation = roomIsGroup
@@ -465,6 +466,10 @@ class DefaultNotificationCreator @Inject constructor(
drawable.draw(canvas)
return bitmap
}
companion object {
const val MESSAGE_EVENT_ID = "message_event_id"
}
}
fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed

View File

@@ -19,12 +19,11 @@ package io.element.android.libraries.push.impl.notifications.model
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
import java.io.Serializable
/**
* Parent interface for all events which can be displayed as a Notification.
*/
sealed interface NotifiableEvent : Serializable {
sealed interface NotifiableEvent {
val sessionId: SessionId
val roomId: RoomId
val eventId: EventId

View File

@@ -0,0 +1,32 @@
/*
* 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
*
* https://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.model
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
sealed interface ResolvedPushEvent {
data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent
data class Redaction(
val sessionId: SessionId,
val roomId: RoomId,
val redactedEventId: EventId,
val reason: String?,
) : ResolvedPushEvent
}

View File

@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData
@@ -41,6 +42,7 @@ private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class)
class DefaultPushHandler @Inject constructor(
private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val onRedactedEventReceived: OnRedactedEventReceived,
private val notifiableEventResolver: NotifiableEventResolver,
private val incrementPushDataStore: IncrementPushDataStore,
private val userPushStoreFactory: UserPushStoreFactory,
@@ -96,19 +98,26 @@ class DefaultPushHandler @Inject constructor(
Timber.w("Unable to get a session")
return
}
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
when (notifiableEvent) {
val resolvedPushEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
when (resolvedPushEvent) {
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
else -> {
val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
if (areNotificationsEnabled) {
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
} else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
is ResolvedPushEvent.Event -> {
when (resolvedPushEvent.notifiableEvent) {
is NotifiableRingingCallEvent -> handleRingingCallEvent(resolvedPushEvent.notifiableEvent)
else -> {
val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
if (areNotificationsEnabled) {
onNotifiableEventReceived.onNotifiableEventReceived(resolvedPushEvent.notifiableEvent)
} else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
}
}
}
}
is ResolvedPushEvent.Redaction -> {
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
}
}
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")

View File

@@ -0,0 +1,97 @@
/*
* 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.push
import android.content.Context
import android.graphics.Typeface
import android.text.style.StyleSpan
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
import io.element.android.libraries.push.impl.notifications.factories.DefaultNotificationCreator
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
interface OnRedactedEventReceived {
fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction)
}
@ContributesBinding(AppScope::class)
class DefaultOnRedactedEventReceived @Inject constructor(
private val activeNotificationsProvider: ActiveNotificationsProvider,
private val notificationDisplayer: NotificationDisplayer,
private val coroutineScope: CoroutineScope,
@ApplicationContext private val context: Context,
private val stringProvider: StringProvider,
) : OnRedactedEventReceived {
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) {
coroutineScope.launch {
val notifications = activeNotificationsProvider.getMessageNotificationsForRoom(
redaction.sessionId,
redaction.roomId,
)
if (notifications.isEmpty()) {
Timber.d("No notifications found for redacted event")
}
notifications.forEach { statusBarNotification ->
val notification = statusBarNotification.notification
val messagingStyle = MessagingStyle.extractMessagingStyleFromNotification(notification)
if (messagingStyle == null) {
Timber.w("Unable to retrieve messaging style from notification")
return@forEach
}
val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message ->
message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) == redaction.redactedEventId.value
}
if (messageToRedactIndex == -1) {
Timber.d("Unable to find the message to remove from notification")
return@forEach
}
val oldMessage = messagingStyle.messages[messageToRedactIndex]
val content = buildSpannedString {
inSpans(StyleSpan(Typeface.ITALIC)) {
append(stringProvider.getString(CommonStrings.common_message_removed))
}
}
val newMessage = MessagingStyle.Message(
content,
oldMessage.timestamp,
oldMessage.person
)
messagingStyle.messages[messageToRedactIndex] = newMessage
notificationDisplayer.showNotificationMessage(
statusBarNotification.tag,
statusBarNotification.id,
NotificationCompat.Builder(context, notification)
.setStyle(messagingStyle)
.build()
)
}
}
}
}

View File

@@ -36,27 +36,6 @@ import org.robolectric.RobolectricTestRunner
class DefaultActiveNotificationsProviderTest {
private val notificationIdProvider = NotificationIdProvider
@Test
fun `getAllNotifications with no active notifications returns empty list`() {
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList())
val emptyNotifications = activeNotificationsProvider.getAllNotifications()
assertThat(emptyNotifications).isEmpty()
}
@Test
fun `getAllNotifications with active notifications returns all`() {
val activeNotifications = listOf(
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
)
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
val result = activeNotificationsProvider.getAllNotifications()
assertThat(result).hasSize(3)
}
@Test
fun `getNotificationsForSession returns only notifications for that session id`() {
val activeNotifications = listOf(

View File

@@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_REDACTION_REASON
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_ID_2
@@ -52,6 +53,7 @@ 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.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
@@ -104,7 +106,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Hello world")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Hello world")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -123,7 +127,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -146,7 +152,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Hello world")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Hello world")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -169,7 +177,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Hello world")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Hello world")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -186,7 +196,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Audio")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Audio")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -203,7 +215,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Video")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Video")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -220,7 +234,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Voice message")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Voice message")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -237,7 +253,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Image")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Image")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -254,7 +272,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Sticker")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Sticker")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -271,7 +291,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "File")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "File")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -288,7 +310,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Location")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Location")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -305,7 +329,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Notice")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Notice")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -322,7 +348,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "* Bob is happy")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "* Bob is happy")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -339,7 +367,9 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = createNotifiableMessageEvent(body = "Poll: A question")
val expectedResult = ResolvedPushEvent.Event(
createNotifiableMessageEvent(body = "Poll: A question")
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -357,21 +387,23 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = InviteNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
canBeReplaced = true,
roomName = null,
noisy = false,
title = null,
description = "Invited you to join the room",
type = null,
timestamp = A_TIMESTAMP,
soundName = null,
isRedacted = false,
isUpdated = false,
val expectedResult = ResolvedPushEvent.Event(
InviteNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
canBeReplaced = true,
roomName = null,
noisy = false,
title = null,
description = "Invited you to join the room",
type = null,
timestamp = A_TIMESTAMP,
soundName = null,
isRedacted = false,
isUpdated = false,
)
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -390,21 +422,23 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = InviteNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
canBeReplaced = true,
roomName = null,
noisy = false,
title = null,
description = "Invited you to chat",
type = null,
timestamp = A_TIMESTAMP,
soundName = null,
isRedacted = false,
isUpdated = false,
val expectedResult = ResolvedPushEvent.Event(
InviteNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
canBeReplaced = true,
roomName = null,
noisy = false,
title = null,
description = "Invited you to chat",
type = null,
timestamp = A_TIMESTAMP,
soundName = null,
isRedacted = false,
isUpdated = false,
)
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -435,16 +469,18 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = FallbackNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
description = "Notification",
canBeReplaced = true,
isRedacted = false,
isUpdated = false,
timestamp = A_FAKE_TIMESTAMP,
val expectedResult = ResolvedPushEvent.Event(
FallbackNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
description = "Notification",
canBeReplaced = true,
isRedacted = false,
isUpdated = false,
timestamp = A_FAKE_TIMESTAMP,
)
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -459,27 +495,29 @@ class DefaultNotifiableEventResolverTest {
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
val expectedResult = NotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
canBeReplaced = false,
senderId = A_USER_ID_2,
noisy = false,
timestamp = A_TIMESTAMP,
senderDisambiguatedDisplayName = "Bob",
body = "Call in progress (unsupported)",
imageUriString = null,
threadId = null,
roomName = null,
roomAvatarPath = null,
senderAvatarPath = null,
soundName = null,
outGoingMessage = false,
outGoingMessageFailed = false,
isRedacted = false,
isUpdated = false
val expectedResult = ResolvedPushEvent.Event(
NotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
canBeReplaced = false,
senderId = A_USER_ID_2,
noisy = false,
timestamp = A_TIMESTAMP,
senderDisambiguatedDisplayName = "Bob",
body = "Call in progress (unsupported)",
imageUriString = null,
threadId = null,
roomName = null,
roomAvatarPath = null,
senderAvatarPath = null,
soundName = null,
outGoingMessage = false,
outGoingMessageFailed = false,
isRedacted = false,
isUpdated = false
)
)
assertThat(result).isEqualTo(expectedResult)
}
@@ -498,21 +536,23 @@ class DefaultNotifiableEventResolverTest {
)
)
)
val expectedResult = NotifiableRingingCallEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
senderId = A_USER_ID_2,
roomName = null,
editedEventId = null,
description = "Incoming call",
timestamp = timestamp,
canBeReplaced = true,
isRedacted = false,
isUpdated = false,
senderDisambiguatedDisplayName = "Bob",
senderAvatarUrl = null,
callNotifyType = CallNotifyType.RING,
val expectedResult = ResolvedPushEvent.Event(
NotifiableRingingCallEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
eventId = AN_EVENT_ID,
senderId = A_USER_ID_2,
roomName = null,
editedEventId = null,
description = "Incoming call",
timestamp = timestamp,
canBeReplaced = true,
isRedacted = false,
isUpdated = false,
senderDisambiguatedDisplayName = "Bob",
senderAvatarUrl = null,
callNotifyType = CallNotifyType.RING,
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result).isEqualTo(expectedResult)
@@ -531,22 +571,24 @@ class DefaultNotifiableEventResolverTest {
)
)
)
val expectedResult = NotifiableMessageEvent(
sessionId = A_SESSION_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
noisy = true,
timestamp = 0L,
senderDisambiguatedDisplayName = "Bob",
senderId = UserId("@bob:server.org"),
body = "\uFE0F Incoming call",
roomId = A_ROOM_ID,
threadId = null,
roomName = null,
canBeReplaced = false,
isRedacted = false,
imageUriString = null,
type = EventType.CALL_NOTIFY,
val expectedResult = ResolvedPushEvent.Event(
NotifiableMessageEvent(
sessionId = A_SESSION_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
noisy = true,
timestamp = 0L,
senderDisambiguatedDisplayName = "Bob",
senderId = UserId("@bob:server.org"),
body = "\uFE0F Incoming call",
roomId = A_ROOM_ID,
threadId = null,
roomName = null,
canBeReplaced = false,
isRedacted = false,
imageUriString = null,
type = EventType.CALL_NOTIFY,
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result).isEqualTo(expectedResult)
@@ -564,27 +606,67 @@ class DefaultNotifiableEventResolverTest {
)
)
)
val expectedResult = NotifiableMessageEvent(
sessionId = A_SESSION_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
noisy = true,
timestamp = A_TIMESTAMP,
senderDisambiguatedDisplayName = "Bob",
senderId = UserId("@bob:server.org"),
body = "\uFE0F Incoming call",
roomId = A_ROOM_ID,
threadId = null,
roomName = null,
canBeReplaced = false,
isRedacted = false,
imageUriString = null,
type = EventType.CALL_NOTIFY,
val expectedResult = ResolvedPushEvent.Event(
NotifiableMessageEvent(
sessionId = A_SESSION_ID,
eventId = AN_EVENT_ID,
editedEventId = null,
noisy = true,
timestamp = A_TIMESTAMP,
senderDisambiguatedDisplayName = "Bob",
senderId = UserId("@bob:server.org"),
body = "\uFE0F Incoming call",
roomId = A_ROOM_ID,
threadId = null,
roomName = null,
canBeReplaced = false,
isRedacted = false,
imageUriString = null,
type = EventType.CALL_NOTIFY,
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result).isEqualTo(expectedResult)
}
@Test
fun `resolve RoomRedaction`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomRedaction(
AN_EVENT_ID_2,
A_REDACTION_REASON,
)
)
)
)
val expectedResult = ResolvedPushEvent.Redaction(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
redactedEventId = AN_EVENT_ID_2,
reason = A_REDACTION_REASON,
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result).isEqualTo(expectedResult)
}
@Test
fun `resolve RoomRedaction with null redactedEventId should return null`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(
createNotificationData(
content = NotificationContent.MessageLike.RoomRedaction(
null,
A_REDACTION_REASON,
)
)
)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result).isNull()
}
@Test
fun `resolve null cases`() {
testNull(NotificationContent.MessageLike.CallAnswer)
@@ -598,7 +680,6 @@ class DefaultNotifiableEventResolverTest {
testNull(NotificationContent.MessageLike.KeyVerificationMac)
testNull(NotificationContent.MessageLike.KeyVerificationDone)
testNull(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
testNull(NotificationContent.MessageLike.RoomRedaction(redactedEventId = AN_EVENT_ID_2.value, reason = null))
testNull(NotificationContent.MessageLike.Sticker)
testNull(NotificationContent.StateEvent.PolicyRuleRoom)
testNull(NotificationContent.StateEvent.PolicyRuleServer)

View File

@@ -72,7 +72,7 @@ class DefaultNotificationDrawerManagerTest {
// For now just call all the API. Later, add more valuable tests.
val matrixUser = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice", avatarUrl = "mxc://data")
val mockRoomGroupMessageCreator = FakeRoomGroupMessageCreator(
createRoomMessageResult = lambdaRecorder { user, _, roomId, _, existingNotification, ->
createRoomMessageResult = lambdaRecorder { user, _, roomId, _, existingNotification ->
assertThat(user).isEqualTo(matrixUser)
assertThat(roomId).isEqualTo(A_ROOM_ID)
assertThat(existingNotification).isNull()
@@ -167,11 +167,12 @@ class DefaultNotificationDrawerManagerTest {
}
val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
val activeNotificationsProvider = FakeActiveNotificationsProvider(
mutableListOf(
getSummaryNotificationResult = {
mockk {
every { id } returns summaryId
}
)
},
countResult = { 1 },
)
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
notificationManager = notificationManager,

View File

@@ -19,13 +19,13 @@ package io.element.android.libraries.push.impl.notifications
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
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.tests.testutils.lambda.lambdaError
class FakeNotifiableEventResolver(
private val notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() }
private val notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() }
) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
return notifiableEventResult(sessionId, roomId, eventId)
}
}

View File

@@ -22,33 +22,34 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider
class FakeActiveNotificationsProvider(
var activeNotifications: MutableList<StatusBarNotification> = mutableListOf(),
private val getMessageNotificationsForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> emptyList() },
private val getNotificationsForSessionResult: (SessionId) -> List<StatusBarNotification> = { emptyList() },
private val getMembershipNotificationForSessionResult: (SessionId) -> List<StatusBarNotification> = { emptyList() },
private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> emptyList() },
private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null },
private val countResult: (SessionId) -> Int = { 0 },
) : ActiveNotificationsProvider {
override fun getAllNotifications(): List<StatusBarNotification> {
return activeNotifications
}
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
return activeNotifications
return getMessageNotificationsForRoomResult(sessionId, roomId)
}
override fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification> {
return activeNotifications
return getNotificationsForSessionResult(sessionId)
}
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
return activeNotifications
return getMembershipNotificationForSessionResult(sessionId)
}
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
return activeNotifications
return getMembershipNotificationForRoomResult(sessionId, roomId)
}
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
return activeNotifications.firstOrNull()
return getSummaryNotificationResult(sessionId)
}
override fun count(sessionId: SessionId): Int {
return activeNotifications.size
return countResult(sessionId)
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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
*
* https://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.push
import android.service.notification.StatusBarNotification
import androidx.test.platform.app.InstrumentationRegistry
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
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.push.impl.notifications.fake.FakeActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.lambdaError
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DefaultOnRedactedEventReceivedTest {
@Test
fun `when no notifications are found, nothing happen`() = runTest {
val sut = createDefaultOnRedactedEventReceived(
getMessageNotificationsForRoomResult = { _, _ -> emptyList() }
)
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))
}
@Test
fun `when a notification is found, try to retrieve the message`() = runTest {
val sut = createDefaultOnRedactedEventReceived(
getMessageNotificationsForRoomResult = { _, _ ->
listOf(
mockk {
every { notification } returns mockk {}
}
)
}
)
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))
}
private fun TestScope.createDefaultOnRedactedEventReceived(
getMessageNotificationsForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> lambdaError() },
): DefaultOnRedactedEventReceived {
val context = InstrumentationRegistry.getInstrumentation().context
return DefaultOnRedactedEventReceived(
activeNotificationsProvider = FakeActiveNotificationsProvider(
getMessageNotificationsForRoomResult = getMessageNotificationsForRoomResult,
getNotificationsForSessionResult = { lambdaError() },
getMembershipNotificationForSessionResult = { lambdaError() },
getMembershipNotificationForRoomResult = { _, _ -> lambdaError() },
getSummaryNotificationResult = { lambdaError() },
countResult = { lambdaError() },
),
notificationDisplayer = FakeNotificationDisplayer(),
coroutineScope = this,
context = context,
stringProvider = FakeStringProvider(),
)
}
}

View File

@@ -30,8 +30,10 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
@@ -40,6 +42,7 @@ import io.element.android.libraries.push.impl.notifications.channels.FakeNotific
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.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData
@@ -61,7 +64,9 @@ class DefaultPushHandlerTest {
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent> { _, _, _ ->
ResolvedPushEvent.Event(aNotifiableMessageEvent)
}
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
@@ -94,7 +99,9 @@ class DefaultPushHandlerTest {
runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
ResolvedPushEvent.Event(aNotifiableMessageEvent)
}
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
@@ -128,7 +135,9 @@ class DefaultPushHandlerTest {
runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
ResolvedPushEvent.Event(aNotifiableMessageEvent)
}
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
@@ -164,7 +173,9 @@ class DefaultPushHandlerTest {
runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
ResolvedPushEvent.Event(aNotifiableMessageEvent)
}
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
@@ -197,7 +208,7 @@ class DefaultPushHandlerTest {
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
runTest {
val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent?> { _, _, _ -> null }
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event?> { _, _, _ -> null }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData(
@@ -240,7 +251,9 @@ class DefaultPushHandlerTest {
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint,
notifiableEventResult = { _, _, _ -> aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) },
notifiableEventResult = { _, _, _ ->
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()))
},
incrementPushCounterResult = {},
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
@@ -265,7 +278,9 @@ class DefaultPushHandlerTest {
val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint,
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent(type = EventType.CALL_NOTIFY) },
notifiableEventResult = { _, _, _ ->
ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY))
},
incrementPushCounterResult = {},
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
@@ -291,7 +306,9 @@ class DefaultPushHandlerTest {
val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint,
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = { _, _, _ -> aNotifiableCallEvent() },
notifiableEventResult = { _, _, _ ->
ResolvedPushEvent.Event(aNotifiableCallEvent())
},
incrementPushCounterResult = {},
userPushStore = FakeUserPushStore().apply {
setNotificationEnabledForDevice(false)
@@ -305,6 +322,37 @@ class DefaultPushHandlerTest {
onNotifiableEventReceived.assertions().isNeverCalled()
}
@Test
fun `when a redaction is received, the onRedactedEventReceived is informed`() = runTest {
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val aRedaction = ResolvedPushEvent.Redaction(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
redactedEventId = AN_EVENT_ID_2,
reason = null
)
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val defaultPushHandler = createDefaultPushHandler(
onRedactedEventReceived = onRedactedEventReceived,
incrementPushCounterResult = incrementPushCounterResult,
notifiableEventResult = { _, _, _ -> aRedaction },
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
),
)
defaultPushHandler.handle(aPushData)
incrementPushCounterResult.assertions()
.isCalledOnce()
onRedactedEventReceived.assertions().isCalledOnce()
.with(value(aRedaction))
}
@Test
fun `when diagnostic PushData is received, the diagnostic push handler is informed`() =
runTest {
@@ -327,7 +375,8 @@ class DefaultPushHandlerTest {
private fun createDefaultPushHandler(
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() },
notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() },
onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() },
incrementPushCounterResult: () -> Unit = { lambdaError() },
userPushStore: UserPushStore = FakeUserPushStore(),
pushClientSecret: PushClientSecret = FakePushClientSecret(),
@@ -339,6 +388,7 @@ class DefaultPushHandlerTest {
): DefaultPushHandler {
return DefaultPushHandler(
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived),
notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult),
incrementPushDataStore = object : IncrementPushDataStore {
override suspend fun incrementPushCounter() {

View File

@@ -0,0 +1,28 @@
/*
* 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.push
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.tests.testutils.lambda.lambdaError
class FakeOnRedactedEventReceived(
private val onRedactedEventReceivedResult: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
) : OnRedactedEventReceived {
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) {
onRedactedEventReceivedResult(redaction)
}
}