diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 74bd2d8e14..b1dd0c683c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -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 diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index fa0b6365f3..39dadffe59 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -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) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index a84fe1edf6..3b39530978 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -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" diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt index 01b9837f3b..c742c375d4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt @@ -26,7 +26,6 @@ import io.element.android.libraries.push.api.notifications.NotificationIdProvide import javax.inject.Inject interface ActiveNotificationsProvider { - fun getAllNotifications(): List fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List fun getNotificationsForSession(sessionId: SessionId): List fun getMembershipNotificationForSession(sessionId: SessionId): List @@ -39,10 +38,6 @@ interface ActiveNotificationsProvider { class DefaultActiveNotificationsProvider @Inject constructor( private val notificationManager: NotificationManagerCompat, ) : ActiveNotificationsProvider { - override fun getAllNotifications(): List { - return notificationManager.activeNotifications - } - override fun getNotificationsForSession(sessionId: SessionId): List { return notificationManager.activeNotifications.filter { it.notification.group == sessionId.value } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index ef97dbf002..7dd9ef9d9d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -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( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index e36966cc9a..181a1bd364 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -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 + ) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index b1b4068efe..096132ffab 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -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 diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt index ddfbbf8b07..282382eaee 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt @@ -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 diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/ResolvedPushEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/ResolvedPushEvent.kt new file mode 100644 index 0000000000..9722d6c3f6 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/ResolvedPushEvent.kt @@ -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 +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index a553c14952..5f33e3134a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -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") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt new file mode 100644 index 0000000000..f632c2bbcc --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt @@ -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() + ) + } + } + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt index 70403e1986..04c2a38a34 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt @@ -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( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index 0c0dbcc495..2623bc8741 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -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) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index bbe5e273b2..7d1e806911 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -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, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt index a9ccabaa6d..234f2f11f7 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt @@ -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) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt index 680688d3dc..d99a4264c0 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt @@ -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 = mutableListOf(), + private val getMessageNotificationsForRoomResult: (SessionId, RoomId) -> List = { _, _ -> emptyList() }, + private val getNotificationsForSessionResult: (SessionId) -> List = { emptyList() }, + private val getMembershipNotificationForSessionResult: (SessionId) -> List = { emptyList() }, + private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List = { _, _ -> emptyList() }, + private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null }, + private val countResult: (SessionId) -> Int = { 0 }, ) : ActiveNotificationsProvider { - override fun getAllNotifications(): List { - return activeNotifications - } - override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List { - return activeNotifications + return getMessageNotificationsForRoomResult(sessionId, roomId) } override fun getNotificationsForSession(sessionId: SessionId): List { - return activeNotifications + return getNotificationsForSessionResult(sessionId) } override fun getMembershipNotificationForSession(sessionId: SessionId): List { - return activeNotifications + return getMembershipNotificationForSessionResult(sessionId) } override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List { - 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) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt new file mode 100644 index 0000000000..bb2f318467 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt @@ -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 = { _, _ -> 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(), + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 160f9f5135..2c287ee294 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -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 { _, _, _ -> aNotifiableMessageEvent } + lambdaRecorder { _, _, _ -> + ResolvedPushEvent.Event(aNotifiableMessageEvent) + } val onNotifiableEventReceived = lambdaRecorder {} val incrementPushCounterResult = lambdaRecorder {} val aPushData = PushData( @@ -94,7 +99,9 @@ class DefaultPushHandlerTest { runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder { _, _, _ -> aNotifiableMessageEvent } + lambdaRecorder { _, _, _ -> + ResolvedPushEvent.Event(aNotifiableMessageEvent) + } val onNotifiableEventReceived = lambdaRecorder {} val incrementPushCounterResult = lambdaRecorder {} val aPushData = PushData( @@ -128,7 +135,9 @@ class DefaultPushHandlerTest { runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder { _, _, _ -> aNotifiableMessageEvent } + lambdaRecorder { _, _, _ -> + ResolvedPushEvent.Event(aNotifiableMessageEvent) + } val onNotifiableEventReceived = lambdaRecorder {} val incrementPushCounterResult = lambdaRecorder {} val aPushData = PushData( @@ -164,7 +173,9 @@ class DefaultPushHandlerTest { runTest { val aNotifiableMessageEvent = aNotifiableMessageEvent() val notifiableEventResult = - lambdaRecorder { _, _, _ -> aNotifiableMessageEvent } + lambdaRecorder { _, _, _ -> + ResolvedPushEvent.Event(aNotifiableMessageEvent) + } val onNotifiableEventReceived = lambdaRecorder {} val incrementPushCounterResult = lambdaRecorder {} 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 { _, _, _ -> null } + lambdaRecorder { _, _, _ -> null } val onNotifiableEventReceived = lambdaRecorder {} val incrementPushCounterResult = lambdaRecorder {} 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 { } + val incrementPushCounterResult = lambdaRecorder {} + 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() { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnRedactedEventReceived.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnRedactedEventReceived.kt new file mode 100644 index 0000000000..6a60243685 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnRedactedEventReceived.kt @@ -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) + } +}