From e95d49a3aeeb01419118d88ca58e0e389ff610f4 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 8 Oct 2021 16:56:50 +0100 Subject: [PATCH 01/49] avoiding dispatching invitation accepted events - we only want to notify users when they receive an invititation, not when they've accepted it --- .../session/notification/ProcessEventForPushTask.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 1321f8dd62..d9e4f967ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -17,7 +17,11 @@ package org.matrix.android.sdk.internal.session.notification import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.Task @@ -51,11 +55,14 @@ internal class DefaultProcessEventForPushTask @Inject constructor( value.timeline?.events?.map { it.copy(roomId = key) } } .flatten() + .filterNot { it.isInvitationJoined() } + val inviteEvents = params.syncResponse.invite .mapNotNull { (key, value) -> value.inviteState?.events?.map { it.copy(roomId = key) } } .flatten() + val allEvents = (newJoinEvents + inviteEvents).filter { event -> when (event.type) { EventType.MESSAGE, @@ -93,3 +100,6 @@ internal class DefaultProcessEventForPushTask @Inject constructor( defaultPushRuleService.dispatchFinish() } } + +private fun Event.isInvitationJoined(): Boolean = type == EventType.STATE_ROOM_MEMBER && + content?.toModel()?.membership == Membership.INVITE From 0c809b5ed1f28eee2ee73ded2d9b5ec850f0df42 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 8 Oct 2021 17:02:59 +0100 Subject: [PATCH 02/49] now that we ignore duplicated invite joined events at the source we can avoid eager notification cancels and rely on the main notification refresh flow --- .../notifications/NotificationDrawerManager.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index e4b2ead93d..45c21bb028 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -212,12 +212,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } fun clearMemberShipNotificationForRoom(roomId: String) { - synchronized(eventList) { - eventList.removeAll { e -> - e is InviteNotifiableEvent && e.roomId == roomId - } + val shouldUpdate = removeAll { it is InviteNotifiableEvent && it.roomId == roomId } + if (shouldUpdate) { + refreshNotificationDrawerBg() + } + } + + private fun removeAll(predicate: (NotifiableEvent) -> Boolean): Boolean { + return synchronized(eventList) { + eventList.removeAll(predicate) } - notificationUtils.cancelNotificationMessage(roomId, ROOM_INVITATION_NOTIFICATION_ID) } private var firstThrottler = FirstThrottler(200) From 37a7d449ae1bbbaae9d05f9c48ab8f3e3bbedd9b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Oct 2021 09:48:41 +0100 Subject: [PATCH 03/49] moving invitiation joined event filtering to the existing mapNotNull chain to avoid another list creation --- .../session/notification/ProcessEventForPushTask.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index d9e4f967ee..865f942ee9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -52,10 +52,11 @@ internal class DefaultProcessEventForPushTask @Inject constructor( } val newJoinEvents = params.syncResponse.join .mapNotNull { (key, value) -> - value.timeline?.events?.map { it.copy(roomId = key) } + value.timeline?.events?.mapNotNull { + it.takeIf { !it.isInvitation() }?.copy(roomId = key) + } } .flatten() - .filterNot { it.isInvitationJoined() } val inviteEvents = params.syncResponse.invite .mapNotNull { (key, value) -> @@ -101,5 +102,5 @@ internal class DefaultProcessEventForPushTask @Inject constructor( } } -private fun Event.isInvitationJoined(): Boolean = type == EventType.STATE_ROOM_MEMBER && +private fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && content?.toModel()?.membership == Membership.INVITE From 1c0d69674d9f081b6f78bd34cf012c150657a884 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Oct 2021 17:09:50 +0100 Subject: [PATCH 04/49] moving is invitation help to the event file --- .../matrix/android/sdk/api/session/events/model/Event.kt | 5 +++++ .../session/notification/ProcessEventForPushTask.kt | 8 +------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 169f90dbca..aad5fce33e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -22,6 +22,8 @@ import org.json.JSONObject import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent @@ -310,3 +312,6 @@ fun Event.isEdition(): Boolean { fun Event.getPresenceContent(): PresenceContent? { return content.toModel() } + +fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && + content?.toModel()?.membership == Membership.INVITE diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 865f942ee9..3c74888eda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -17,11 +17,8 @@ package org.matrix.android.sdk.internal.session.notification import org.matrix.android.sdk.api.pushrules.rest.PushRule -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.events.model.isInvitation import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.Task @@ -101,6 +98,3 @@ internal class DefaultProcessEventForPushTask @Inject constructor( defaultPushRuleService.dispatchFinish() } } - -private fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && - content?.toModel()?.membership == Membership.INVITE From 67211605aad72df13b6f1fa56d1aff33ef27f538 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 10:35:41 +0100 Subject: [PATCH 05/49] removing unused commented code --- .../fcm/VectorFirebaseMessagingService.kt | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index b1f0b43705..fadbeaa647 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -227,87 +227,4 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { } return false } - - private fun handleNotificationWithoutSyncingMode(data: Map, session: Session?) { - if (session == null) { - Timber.tag(loggerTag.value).e("## handleNotificationWithoutSyncingMode cannot find session") - return - } - - // The Matrix event ID of the event being notified about. - // This is required if the notification is about a particular Matrix event. - // It may be omitted for notifications that only contain updated badge counts. - // This ID can and should be used to detect duplicate notification requests. - val eventId = data["event_id"] ?: return // Just ignore - - val eventType = data["type"] - if (eventType == null) { - // Just add a generic unknown event - val simpleNotifiableEvent = SimpleNotifiableEvent( - session.myUserId, - eventId, - null, - true, // It's an issue in this case, all event will bing even if expected to be silent. - title = getString(R.string.notification_unknown_new_event), - description = "", - type = null, - timestamp = System.currentTimeMillis(), - soundName = Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT, - isPushGatewayEvent = true - ) - notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) - notificationDrawerManager.refreshNotificationDrawer() - } else { - val event = parseEvent(data) ?: return - - val notifiableEvent = notifiableEventResolver.resolveEvent(event, session) - - if (notifiableEvent == null) { - Timber.tag(loggerTag.value).e("Unsupported notifiable event $eventId") - if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.tag(loggerTag.value).e("--> $event") - } - } else { - if (notifiableEvent is NotifiableMessageEvent) { - if (notifiableEvent.senderName.isNullOrEmpty()) { - notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: "" - } - if (notifiableEvent.roomName.isNullOrEmpty()) { - notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: "" - } - } - - notifiableEvent.isPushGatewayEvent = true - notifiableEvent.matrixID = session.myUserId - notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) - notificationDrawerManager.refreshNotificationDrawer() - } - } - } - - private fun findRoomNameBestEffort(data: Map, session: Session?): String? { - var roomName: String? = data["room_name"] - val roomId = data["room_id"] - if (null == roomName && null != roomId) { - // Try to get the room name from our store - roomName = session?.getRoom(roomId)?.roomSummary()?.displayName - } - return roomName - } - - /** - * Try to create an event from the FCM data - * - * @param data the FCM data - * @return the event or null if required data are missing - */ - private fun parseEvent(data: Map?): Event? { - return Event( - eventId = data?.get("event_id") ?: return null, - senderId = data["sender"], - roomId = data["room_id"] ?: return null, - type = data["type"] ?: return null, - originServerTs = System.currentTimeMillis() - ) - } } From 51f7dee95232a905ca62a21abf91673ecd53a26e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 11:09:30 +0100 Subject: [PATCH 06/49] removing non common properties form the base event --- .../notifications/InviteNotifiableEvent.kt | 14 +++++++------- .../app/features/notifications/NotifiableEvent.kt | 7 ------- .../notifications/NotifiableMessageEvent.kt | 14 ++++++-------- .../notifications/NotificationDrawerManager.kt | 4 ++-- .../notifications/SimpleNotifiableEvent.kt | 12 ++++++------ 5 files changed, 21 insertions(+), 30 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 61fd5c677a..488da60129 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -18,19 +18,19 @@ package im.vector.app.features.notifications import androidx.core.app.NotificationCompat data class InviteNotifiableEvent( - override var matrixID: String?, + var matrixID: String?, override val eventId: String, override val editedEventId: String?, var roomId: String, override var noisy: Boolean, - override val title: String, - override val description: String, - override val type: String?, - override val timestamp: Long, - override var soundName: String?, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + var soundName: String?, override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { - override var hasBeenDisplayed: Boolean = false override var isRedacted: Boolean = false override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC + override var hasBeenDisplayed = false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index a4f099b905..07833697b4 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -21,20 +21,13 @@ import java.io.Serializable * Parent interface for all events which can be displayed as a Notification */ interface NotifiableEvent : Serializable { - var matrixID: String? val eventId: String val editedEventId: String? var noisy: Boolean - val title: String - val description: String? - val type: String? - val timestamp: Long // NotificationCompat.VISIBILITY_PUBLIC , VISIBILITY_PRIVATE , VISIBILITY_SECRET var lockScreenVisibility: Int - // Compat: Only for android <7, for newer version the sound is defined in the channel - var soundName: String? var hasBeenDisplayed: Boolean var isRedacted: Boolean diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index fb9ca8d23c..721325e436 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -22,7 +22,7 @@ data class NotifiableMessageEvent( override val eventId: String, override val editedEventId: String?, override var noisy: Boolean, - override val timestamp: Long, + val timestamp: Long, var senderName: String?, var senderId: String?, var body: String?, @@ -31,8 +31,8 @@ data class NotifiableMessageEvent( var roomIsDirect: Boolean = false ) : NotifiableEvent { - override var matrixID: String? = null - override var soundName: String? = null + var matrixID: String? = null + var soundName: String? = null override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var hasBeenDisplayed: Boolean = false override var isRedacted: Boolean = false @@ -42,14 +42,12 @@ data class NotifiableMessageEvent( override var isPushGatewayEvent: Boolean = false - override val type: String - get() = EventType.MESSAGE + val type: String = EventType.MESSAGE - override val description: String? + val description: String get() = body ?: "" - override val title: String - get() = senderName ?: "" + val title: String = senderName ?: "" // This is used for >N notification, as the result of a smart reply var outGoingMessage = false diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 45c21bb028..8648490c6a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -395,7 +395,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context textStyle = "bold" +String.format("%s: ", event.senderName) } - +(event.description ?: "") + +(event.description) } summaryInboxStyle.addLine(line) } else { @@ -404,7 +404,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context textStyle = "bold" +String.format("%s: %s ", roomName, event.senderName) } - +(event.description ?: "") + +(event.description) } summaryInboxStyle.addLine(line) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index 2f74737ba2..dbc04c6f65 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -18,15 +18,15 @@ package im.vector.app.features.notifications import androidx.core.app.NotificationCompat data class SimpleNotifiableEvent( - override var matrixID: String?, + var matrixID: String?, override val eventId: String, override val editedEventId: String?, override var noisy: Boolean, - override val title: String, - override val description: String, - override val type: String?, - override val timestamp: Long, - override var soundName: String?, + val title: String, + val description: String, + val type: String?, + val timestamp: Long, + var soundName: String?, override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false From 81da185d8b3a81bb43af5169f705b33b8d3bcaf3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 11:23:23 +0100 Subject: [PATCH 07/49] making non overriden properties immutable by passing the values intro the constructor --- .../notifications/InviteNotifiableEvent.kt | 6 +-- .../notifications/NotifiableEventResolver.kt | 44 ++++++++----------- .../notifications/NotifiableMessageEvent.kt | 37 +++++++--------- .../NotificationBroadcastReceiver.kt | 22 +++++----- .../notifications/SimpleNotifiableEvent.kt | 4 +- 5 files changed, 52 insertions(+), 61 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 488da60129..7500ee3993 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -18,16 +18,16 @@ package im.vector.app.features.notifications import androidx.core.app.NotificationCompat data class InviteNotifiableEvent( - var matrixID: String?, + val matrixID: String?, override val eventId: String, override val editedEventId: String?, - var roomId: String, + val roomId: String, override var noisy: Boolean, val title: String, val description: String, val type: String?, val timestamp: Long, - var soundName: String?, + val soundName: String?, override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var isRedacted: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 63c296f418..fa76a40835 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -142,7 +142,7 @@ class NotifiableEventResolver @Inject constructor( val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.senderInfo.disambiguatedDisplayName - val notifiableEvent = NotifiableMessageEvent( + return NotifiableMessageEvent( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, @@ -151,10 +151,9 @@ class NotifiableEventResolver @Inject constructor( senderId = event.root.senderId, body = body.toString(), roomId = event.root.roomId!!, - roomName = roomName) - - notifiableEvent.matrixID = session.myUserId - return notifiableEvent + roomName = roomName, + matrixID = session.myUserId + ) } else { if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) { // TODO use a global event decryptor? attache to session and that listen to new sessionId? @@ -175,7 +174,7 @@ class NotifiableEventResolver @Inject constructor( val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderInfo.disambiguatedDisplayName - val notifiableEvent = NotifiableMessageEvent( + return NotifiableMessageEvent( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, @@ -185,25 +184,20 @@ class NotifiableEventResolver @Inject constructor( body = body, roomId = event.root.roomId!!, roomName = roomName, - roomIsDirect = room.roomSummary()?.isDirect ?: false) - - notifiableEvent.matrixID = session.myUserId - notifiableEvent.soundName = null - - // Get the avatars URL - notifiableEvent.roomAvatarPath = session.contentUrlResolver() - .resolveThumbnail(room.roomSummary()?.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE) - - notifiableEvent.senderAvatarPath = session.contentUrlResolver() - .resolveThumbnail(event.senderInfo.avatarUrl, - 250, - 250, - ContentUrlResolver.ThumbnailMethod.SCALE) - - return notifiableEvent + roomIsDirect = room.roomSummary()?.isDirect ?: false, + roomAvatarPath = session.contentUrlResolver() + .resolveThumbnail(room.roomSummary()?.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE), + senderAvatarPath = session.contentUrlResolver() + .resolveThumbnail(event.senderInfo.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE), + matrixID = session.myUserId, + soundName = null + ) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index 721325e436..9370af5f5e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -23,33 +23,30 @@ data class NotifiableMessageEvent( override val editedEventId: String?, override var noisy: Boolean, val timestamp: Long, - var senderName: String?, - var senderId: String?, - var body: String?, - var roomId: String, - var roomName: String?, - var roomIsDirect: Boolean = false + val senderName: String?, + val senderId: String?, + val body: String?, + val roomId: String, + val roomName: String?, + val roomIsDirect: Boolean = false, + val roomAvatarPath: String? = null, + val senderAvatarPath: String? = null, + + val matrixID: String? = null, + val soundName: String? = null, + + // This is used for >N notification, as the result of a smart reply + val outGoingMessage: Boolean = false, + val outGoingMessageFailed: Boolean = false + ) : NotifiableEvent { - var matrixID: String? = null - var soundName: String? = null override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var hasBeenDisplayed: Boolean = false override var isRedacted: Boolean = false - - var roomAvatarPath: String? = null - var senderAvatarPath: String? = null - override var isPushGatewayEvent: Boolean = false val type: String = EventType.MESSAGE - - val description: String - get() = body ?: "" - + val description: String = body ?: "" val title: String = senderName ?: "" - - // This is used for >N notification, as the result of a smart reply - var outGoingMessage = false - var outGoingMessageFailed = false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 6583db6f69..60dfcca7a1 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -130,19 +130,19 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val notifiableMessageEvent = NotifiableMessageEvent( // Generate a Fake event id - UUID.randomUUID().toString(), - null, - false, - System.currentTimeMillis(), - session.getRoomMember(session.myUserId, room.roomId)?.displayName + eventId = UUID.randomUUID().toString(), + editedEventId = null, + noisy = false, + timestamp = System.currentTimeMillis(), + senderName = session.getRoomMember(session.myUserId, room.roomId)?.displayName ?: context?.getString(R.string.notification_sender_me), - session.myUserId, - message, - room.roomId, - room.roomSummary()?.displayName ?: room.roomId, - room.roomSummary()?.isDirect == true + senderId = session.myUserId, + body = message, + roomId = room.roomId, + roomName = room.roomSummary()?.displayName ?: room.roomId, + roomIsDirect = room.roomSummary()?.isDirect == true, + outGoingMessage = true ) - notifiableMessageEvent.outGoingMessage = true notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) notificationDrawerManager.refreshNotificationDrawer() diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index dbc04c6f65..5d470158d9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -18,7 +18,7 @@ package im.vector.app.features.notifications import androidx.core.app.NotificationCompat data class SimpleNotifiableEvent( - var matrixID: String?, + val matrixID: String?, override val eventId: String, override val editedEventId: String?, override var noisy: Boolean, @@ -26,7 +26,7 @@ data class SimpleNotifiableEvent( val description: String, val type: String?, val timestamp: Long, - var soundName: String?, + val soundName: String?, override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false From 89d643a4be739a7d45b5a7e7a6b91867108eaa74 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 11:28:54 +0100 Subject: [PATCH 08/49] removing unused property (written to but never read) --- .../app/features/notifications/InviteNotifiableEvent.kt | 3 --- .../im/vector/app/features/notifications/NotifiableEvent.kt | 3 --- .../app/features/notifications/NotifiableEventResolver.kt | 5 +---- .../app/features/notifications/NotifiableMessageEvent.kt | 2 -- .../app/features/notifications/SimpleNotifiableEvent.kt | 3 --- 5 files changed, 1 insertion(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 7500ee3993..91cc216759 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -15,8 +15,6 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat - data class InviteNotifiableEvent( val matrixID: String?, override val eventId: String, @@ -31,6 +29,5 @@ data class InviteNotifiableEvent( override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var isRedacted: Boolean = false - override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var hasBeenDisplayed = false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index 07833697b4..279bc192fd 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -25,9 +25,6 @@ interface NotifiableEvent : Serializable { val editedEventId: String? var noisy: Boolean - // NotificationCompat.VISIBILITY_PUBLIC , VISIBILITY_PRIVATE , VISIBILITY_SECRET - var lockScreenVisibility: Int - var hasBeenDisplayed: Boolean var isRedacted: Boolean diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index fa76a40835..9bc4194e49 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -15,7 +15,6 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider @@ -66,9 +65,7 @@ class NotifiableEventResolver @Inject constructor( return resolveMessageEvent(timelineEvent, session) } EventType.ENCRYPTED -> { - val messageEvent = resolveMessageEvent(timelineEvent, session) - messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE - return messageEvent + return resolveMessageEvent(timelineEvent, session) } else -> { // If the event can be displayed, display it as is diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index 9370af5f5e..f23c84afad 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -15,7 +15,6 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat import org.matrix.android.sdk.api.session.events.model.EventType data class NotifiableMessageEvent( @@ -41,7 +40,6 @@ data class NotifiableMessageEvent( ) : NotifiableEvent { - override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var hasBeenDisplayed: Boolean = false override var isRedacted: Boolean = false override var isPushGatewayEvent: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index 5d470158d9..7e776222af 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -15,8 +15,6 @@ */ package im.vector.app.features.notifications -import androidx.core.app.NotificationCompat - data class SimpleNotifiableEvent( val matrixID: String?, override val eventId: String, @@ -31,5 +29,4 @@ data class SimpleNotifiableEvent( override var hasBeenDisplayed: Boolean = false override var isRedacted: Boolean = false - override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC } From c99dd4a615d672cef35c0029e156c114c21ea163 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 11:46:13 +0100 Subject: [PATCH 09/49] making the isRedacted event property immutable - also makes the notifiable events sealed interfaces so that we can copy the data classes with new redacted values when it changes --- .../notifications/InviteNotifiableEvent.kt | 5 +++-- .../features/notifications/NotifiableEvent.kt | 6 ++---- .../notifications/NotifiableMessageEvent.kt | 7 +++---- .../notifications/NotificationDrawerManager.kt | 17 ++++++++++++++--- .../notifications/SimpleNotifiableEvent.kt | 6 ++++-- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 91cc216759..9f958965ea 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -26,8 +26,9 @@ data class InviteNotifiableEvent( val type: String?, val timestamp: Long, val soundName: String?, - override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { + override var isPushGatewayEvent: Boolean = false, + override val isRedacted: Boolean = false +) : NotifiableEvent { - override var isRedacted: Boolean = false override var hasBeenDisplayed = false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index 279bc192fd..6365b1c7bf 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -20,14 +20,12 @@ import java.io.Serializable /** * Parent interface for all events which can be displayed as a Notification */ -interface NotifiableEvent : Serializable { +sealed interface NotifiableEvent : Serializable { val eventId: String val editedEventId: String? var noisy: Boolean - var hasBeenDisplayed: Boolean - var isRedacted: Boolean - // Used to know if event should be replaced with the one coming from eventstream var isPushGatewayEvent: Boolean + val isRedacted: Boolean } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index f23c84afad..7d9ef42f8e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -36,12 +36,11 @@ data class NotifiableMessageEvent( // This is used for >N notification, as the result of a smart reply val outGoingMessage: Boolean = false, - val outGoingMessageFailed: Boolean = false - + val outGoingMessageFailed: Boolean = false, + override var hasBeenDisplayed: Boolean = false, + override val isRedacted: Boolean = false ) : NotifiableEvent { - override var hasBeenDisplayed: Boolean = false - override var isRedacted: Boolean = false override var isPushGatewayEvent: Boolean = false val type: String = EventType.MESSAGE diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 8648490c6a..5496dd6649 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -162,9 +162,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context fun onEventRedacted(eventId: String) { synchronized(eventList) { - eventList.find { it.eventId == eventId }?.apply { - isRedacted = true - hasBeenDisplayed = false + eventList.replace(eventId) { + when (it) { + is InviteNotifiableEvent -> it.copy(isRedacted = true).apply { hasBeenDisplayed = false } + is NotifiableMessageEvent -> it.copy(isRedacted = true).apply { hasBeenDisplayed = false } + is SimpleNotifiableEvent -> it.copy(isRedacted = true).apply { hasBeenDisplayed = false } + } } } } @@ -665,3 +668,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr" } } + +private fun MutableList.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) { + val indexToReplace = indexOfFirst { it.eventId == eventId } + if (indexToReplace == -1) { + return + } + set(indexToReplace, block(get(indexToReplace))) +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index 7e776222af..49a69fc51a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -25,8 +25,10 @@ data class SimpleNotifiableEvent( val type: String?, val timestamp: Long, val soundName: String?, - override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { + override var isPushGatewayEvent: Boolean = false, + override val isRedacted: Boolean = false +) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false - override var isRedacted: Boolean = false + } From db5d4ead38a466e052d6702efc82561ab4c278b0 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 11:52:54 +0100 Subject: [PATCH 10/49] making the noisy property immutable --- .../notifications/InviteNotifiableEvent.kt | 2 +- .../features/notifications/NotifiableEvent.kt | 1 - .../notifications/NotifiableEventResolver.kt | 30 +++++++------------ .../notifications/NotifiableMessageEvent.kt | 2 +- .../notifications/PushRuleTriggerListener.kt | 3 +- .../notifications/SimpleNotifiableEvent.kt | 2 +- 6 files changed, 14 insertions(+), 26 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 9f958965ea..81951df8ef 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -20,7 +20,7 @@ data class InviteNotifiableEvent( override val eventId: String, override val editedEventId: String?, val roomId: String, - override var noisy: Boolean, + val noisy: Boolean, val title: String, val description: String, val type: String?, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index 6365b1c7bf..d6b7181610 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -23,7 +23,6 @@ import java.io.Serializable sealed interface NotifiableEvent : Serializable { val eventId: String val editedEventId: String? - var noisy: Boolean var hasBeenDisplayed: Boolean // Used to know if event should be replaced with the one coming from eventstream var isPushGatewayEvent: Boolean diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 9bc4194e49..c2ffb0b1b3 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -53,19 +53,19 @@ class NotifiableEventResolver @Inject constructor( // private val eventDisplay = RiotEventDisplay(context) - fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session): NotifiableEvent? { + fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? { val roomID = event.roomId ?: return null val eventId = event.eventId ?: return null if (event.getClearType() == EventType.STATE_ROOM_MEMBER) { - return resolveStateRoomEvent(event, session) + return resolveStateRoomEvent(event, session, isNoisy) } val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null when (event.getClearType()) { EventType.MESSAGE -> { - return resolveMessageEvent(timelineEvent, session) + return resolveMessageEvent(timelineEvent, session, isNoisy) } EventType.ENCRYPTED -> { - return resolveMessageEvent(timelineEvent, session) + return resolveMessageEvent(timelineEvent, session, isNoisy) } else -> { // If the event can be displayed, display it as is @@ -111,24 +111,14 @@ class NotifiableEventResolver @Inject constructor( avatarUrl = user.avatarUrl ) ) - - val notifiableEvent = resolveMessageEvent(timelineEvent, session) - - if (notifiableEvent == null) { - Timber.d("## Failed to resolve event") - // TODO - null - } else { - notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() - notifiableEvent - } + resolveMessageEvent(timelineEvent, session, isNoisy = !notificationAction.soundName.isNullOrBlank()) } else { Timber.d("Matched push rule is set to not notify") null } } - private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? { + private fun resolveMessageEvent(event: TimelineEvent, session: Session, isNoisy: Boolean): NotifiableEvent { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) @@ -143,7 +133,7 @@ class NotifiableEventResolver @Inject constructor( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, - noisy = false, // will be updated + noisy = isNoisy, senderName = senderDisplayName, senderId = event.root.senderId, body = body.toString(), @@ -175,7 +165,7 @@ class NotifiableEventResolver @Inject constructor( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, - noisy = false, // will be updated + noisy = isNoisy, senderName = senderDisplayName, senderId = event.root.senderId, body = body, @@ -198,7 +188,7 @@ class NotifiableEventResolver @Inject constructor( } } - private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? { + private fun resolveStateRoomEvent(event: Event, session: Session, isNoisy: Boolean): NotifiableEvent? { val content = event.content?.toModel() ?: return null val roomId = event.roomId ?: return null val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName } @@ -211,7 +201,7 @@ class NotifiableEventResolver @Inject constructor( editedEventId = null, roomId = roomId, timestamp = event.originServerTs ?: 0, - noisy = false, // will be set later + noisy = isNoisy, title = stringProvider.getString(R.string.notification_new_invitation), description = body.toString(), soundName = null, // will be set later diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index 7d9ef42f8e..7f0c83ef7a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -20,7 +20,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType data class NotifiableMessageEvent( override val eventId: String, override val editedEventId: String?, - override var noisy: Boolean, + val noisy: Boolean, val timestamp: Long, val senderName: String?, val senderId: String?, diff --git a/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt index 791803fa49..abbbd47f95 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt @@ -40,12 +40,11 @@ class PushRuleTriggerListener @Inject constructor( val notificationAction = actions.toNotificationAction() if (notificationAction.shouldNotify) { - val notifiableEvent = resolver.resolveEvent(event, safeSession) + val notifiableEvent = resolver.resolveEvent(event, safeSession, isNoisy = !notificationAction.soundName.isNullOrBlank()) if (notifiableEvent == null) { Timber.v("## Failed to resolve event") // TODO } else { - notifiableEvent.noisy = !notificationAction.soundName.isNullOrBlank() Timber.v("New event to notify") notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index 49a69fc51a..a6f45a0f72 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -19,7 +19,7 @@ data class SimpleNotifiableEvent( val matrixID: String?, override val eventId: String, override val editedEventId: String?, - override var noisy: Boolean, + val noisy: Boolean, val title: String, val description: String, val type: String?, From b44a382893512a38bafe93263954cabcbc7a3e0f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 11:57:32 +0100 Subject: [PATCH 11/49] separating the mutable vars from the immutable ones, they'll be removed or made immutable by the notification redesign --- .../app/features/notifications/InviteNotifiableEvent.kt | 2 +- .../app/features/notifications/NotifiableEventResolver.kt | 4 ++-- .../app/features/notifications/NotifiableMessageEvent.kt | 4 +--- .../app/features/notifications/SimpleNotifiableEvent.kt | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 81951df8ef..742be02eb5 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -26,9 +26,9 @@ data class InviteNotifiableEvent( val type: String?, val timestamp: Long, val soundName: String?, - override var isPushGatewayEvent: Boolean = false, override val isRedacted: Boolean = false ) : NotifiableEvent { + override var isPushGatewayEvent: Boolean = false override var hasBeenDisplayed = false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index c2ffb0b1b3..a5ea176a86 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -205,8 +205,8 @@ class NotifiableEventResolver @Inject constructor( title = stringProvider.getString(R.string.notification_new_invitation), description = body.toString(), soundName = null, // will be set later - type = event.getClearType(), - isPushGatewayEvent = false) + type = event.getClearType() + ) } else { Timber.e("## unsupported notifiable event for eventId [${event.eventId}]") if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index 7f0c83ef7a..b806002a99 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -30,18 +30,16 @@ data class NotifiableMessageEvent( val roomIsDirect: Boolean = false, val roomAvatarPath: String? = null, val senderAvatarPath: String? = null, - val matrixID: String? = null, val soundName: String? = null, - // This is used for >N notification, as the result of a smart reply val outGoingMessage: Boolean = false, val outGoingMessageFailed: Boolean = false, - override var hasBeenDisplayed: Boolean = false, override val isRedacted: Boolean = false ) : NotifiableEvent { override var isPushGatewayEvent: Boolean = false + override var hasBeenDisplayed: Boolean = false val type: String = EventType.MESSAGE val description: String = body ?: "" diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index a6f45a0f72..c7aaf4aa6c 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -25,10 +25,10 @@ data class SimpleNotifiableEvent( val type: String?, val timestamp: Long, val soundName: String?, - override var isPushGatewayEvent: Boolean = false, override val isRedacted: Boolean = false ) : NotifiableEvent { + override var isPushGatewayEvent: Boolean = false override var hasBeenDisplayed: Boolean = false } From 86b500445fe0a15099a907d0606ad8c96da8dd39 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 12:19:35 +0100 Subject: [PATCH 12/49] updating the push gateway property to reflect that it mean the event can be replaced - makes the property immutable as only the creation of the event knows if it can be replace eg it came from a push or the /sync event stream --- .../fcm/VectorFirebaseMessagingService.kt | 8 +------ .../notifications/InviteNotifiableEvent.kt | 2 +- .../features/notifications/NotifiableEvent.kt | 2 +- .../notifications/NotifiableEventResolver.kt | 21 ++++++++++++------- .../notifications/NotifiableMessageEvent.kt | 2 +- .../NotificationBroadcastReceiver.kt | 3 ++- .../NotificationDrawerManager.kt | 4 ++-- .../notifications/SimpleNotifiableEvent.kt | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index fadbeaa647..63d50d4f97 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -29,16 +29,13 @@ import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.PushersManager import im.vector.app.features.badge.BadgeProxy import im.vector.app.features.notifications.NotifiableEventResolver -import im.vector.app.features.notifications.NotifiableMessageEvent import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils -import im.vector.app.features.notifications.SimpleNotifiableEvent import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.push.fcm.FcmHelper @@ -48,9 +45,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.pushrules.Action import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.Event import timber.log.Timber import javax.inject.Inject @@ -201,12 +196,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Timber.tag(loggerTag.value).d("Fast lane: start request") val event = tryOrNull { session.getEvent(roomId, eventId) } ?: return@launch - val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event) + val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true) resolvedEvent ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") } ?.let { - it.isPushGatewayEvent = true notificationDrawerManager.onNotifiableEventReceived(it) notificationDrawerManager.refreshNotificationDrawer() } diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 742be02eb5..2d891041b1 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -19,6 +19,7 @@ data class InviteNotifiableEvent( val matrixID: String?, override val eventId: String, override val editedEventId: String?, + override val canBeReplaced: Boolean, val roomId: String, val noisy: Boolean, val title: String, @@ -29,6 +30,5 @@ data class InviteNotifiableEvent( override val isRedacted: Boolean = false ) : NotifiableEvent { - override var isPushGatewayEvent: Boolean = false override var hasBeenDisplayed = false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index d6b7181610..d9c0c22116 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -25,6 +25,6 @@ sealed interface NotifiableEvent : Serializable { val editedEventId: String? var hasBeenDisplayed: Boolean // Used to know if event should be replaced with the one coming from eventstream - var isPushGatewayEvent: Boolean + val canBeReplaced: Boolean val isRedacted: Boolean } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index a5ea176a86..552f96b5a0 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -57,15 +57,15 @@ class NotifiableEventResolver @Inject constructor( val roomID = event.roomId ?: return null val eventId = event.eventId ?: return null if (event.getClearType() == EventType.STATE_ROOM_MEMBER) { - return resolveStateRoomEvent(event, session, isNoisy) + return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) } val timelineEvent = session.getRoom(roomID)?.getTimeLineEvent(eventId) ?: return null when (event.getClearType()) { EventType.MESSAGE -> { - return resolveMessageEvent(timelineEvent, session, isNoisy) + return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) } EventType.ENCRYPTED -> { - return resolveMessageEvent(timelineEvent, session, isNoisy) + return resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) } else -> { // If the event can be displayed, display it as is @@ -82,12 +82,14 @@ class NotifiableEventResolver @Inject constructor( description = bodyPreview, title = stringProvider.getString(R.string.notification_unknown_new_event), soundName = null, - type = event.type) + type = event.type, + canBeReplaced = false + ) } } } - fun resolveInMemoryEvent(session: Session, event: Event): NotifiableEvent? { + fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? { if (event.getClearType() != EventType.MESSAGE) return null // Ignore message edition @@ -111,14 +113,14 @@ class NotifiableEventResolver @Inject constructor( avatarUrl = user.avatarUrl ) ) - resolveMessageEvent(timelineEvent, session, isNoisy = !notificationAction.soundName.isNullOrBlank()) + resolveMessageEvent(timelineEvent, session, canBeReplaced = canBeReplaced, isNoisy = !notificationAction.soundName.isNullOrBlank()) } else { Timber.d("Matched push rule is set to not notify") null } } - private fun resolveMessageEvent(event: TimelineEvent, session: Session, isNoisy: Boolean): NotifiableEvent { + private fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) @@ -132,6 +134,7 @@ class NotifiableEventResolver @Inject constructor( return NotifiableMessageEvent( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, timestamp = event.root.originServerTs ?: 0, noisy = isNoisy, senderName = senderDisplayName, @@ -164,6 +167,7 @@ class NotifiableEventResolver @Inject constructor( return NotifiableMessageEvent( eventId = event.root.eventId!!, editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, timestamp = event.root.originServerTs ?: 0, noisy = isNoisy, senderName = senderDisplayName, @@ -188,7 +192,7 @@ class NotifiableEventResolver @Inject constructor( } } - private fun resolveStateRoomEvent(event: Event, session: Session, isNoisy: Boolean): NotifiableEvent? { + private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? { val content = event.content?.toModel() ?: return null val roomId = event.roomId ?: return null val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName } @@ -199,6 +203,7 @@ class NotifiableEventResolver @Inject constructor( session.myUserId, eventId = event.eventId!!, editedEventId = null, + canBeReplaced = canBeReplaced, roomId = roomId, timestamp = event.originServerTs ?: 0, noisy = isNoisy, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index b806002a99..4a2152c417 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType data class NotifiableMessageEvent( override val eventId: String, override val editedEventId: String?, + override val canBeReplaced: Boolean, val noisy: Boolean, val timestamp: Long, val senderName: String?, @@ -38,7 +39,6 @@ data class NotifiableMessageEvent( override val isRedacted: Boolean = false ) : NotifiableEvent { - override var isPushGatewayEvent: Boolean = false override var hasBeenDisplayed: Boolean = false val type: String = EventType.MESSAGE diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 60dfcca7a1..33e43cd7e4 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -141,7 +141,8 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { roomId = room.roomId, roomName = room.roomSummary()?.displayName ?: room.roomId, roomIsDirect = room.roomSummary()?.isDirect == true, - outGoingMessage = true + outGoingMessage = true, + canBeReplaced = false ) notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 5496dd6649..8c0e5fee5f 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -107,12 +107,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { Timber.d("onNotifiableEventReceived(): $notifiableEvent") } else { - Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.isPushGatewayEvent}") + Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") } synchronized(eventList) { val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId } if (existing != null) { - if (existing.isPushGatewayEvent) { + if (existing.canBeReplaced) { // Use the event coming from the event stream as it may contains more info than // the fcm one (like type/content/clear text) (e.g when an encrypted message from // FCM should be update with clear text after a sync) diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index c7aaf4aa6c..ca9b3f014e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -25,10 +25,10 @@ data class SimpleNotifiableEvent( val type: String?, val timestamp: Long, val soundName: String?, + override var canBeReplaced: Boolean, override val isRedacted: Boolean = false ) : NotifiableEvent { - override var isPushGatewayEvent: Boolean = false override var hasBeenDisplayed: Boolean = false } From 56e2b79774c9f9c3480b3e118a8551817ecdb4d3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 12:22:44 +0100 Subject: [PATCH 13/49] formatting --- .../java/im/vector/app/features/notifications/NotifiableEvent.kt | 1 + .../vector/app/features/notifications/SimpleNotifiableEvent.kt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index d9c0c22116..2f79da6795 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -24,6 +24,7 @@ sealed interface NotifiableEvent : Serializable { val eventId: String val editedEventId: String? var hasBeenDisplayed: Boolean + // Used to know if event should be replaced with the one coming from eventstream val canBeReplaced: Boolean val isRedacted: Boolean diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index ca9b3f014e..940d8a3770 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -30,5 +30,4 @@ data class SimpleNotifiableEvent( ) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false - } From beff5ab821c544acd8ef711f079726b08b9989d9 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 12 Oct 2021 17:27:21 +0100 Subject: [PATCH 14/49] including the room name in the invitation event if the room sumary is available --- .../app/features/notifications/InviteNotifiableEvent.kt | 1 + .../app/features/notifications/NotifiableEventResolver.kt | 4 +++- .../im/vector/app/features/notifications/NotificationUtils.kt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 2d891041b1..743b3587a8 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -21,6 +21,7 @@ data class InviteNotifiableEvent( override val editedEventId: String?, override val canBeReplaced: Boolean, val roomId: String, + val roomName: String?, val noisy: Boolean, val title: String, val description: String, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 552f96b5a0..d2db73af3d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -197,7 +197,8 @@ class NotifiableEventResolver @Inject constructor( val roomId = event.roomId ?: return null val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName } if (Membership.INVITE == content.membership) { - val body = noticeEventFormatter.format(event, dName, isDm = session.getRoomSummary(roomId)?.isDirect.orFalse()) + val roomSummary = session.getRoomSummary(roomId) + val body = noticeEventFormatter.format(event, dName, isDm = roomSummary?.isDirect.orFalse()) ?: stringProvider.getString(R.string.notification_new_invitation) return InviteNotifiableEvent( session.myUserId, @@ -205,6 +206,7 @@ class NotifiableEventResolver @Inject constructor( editedEventId = null, canBeReplaced = canBeReplaced, roomId = roomId, + roomName = roomSummary?.displayName, timestamp = event.originServerTs ?: 0, noisy = isNoisy, title = stringProvider.getString(R.string.notification_new_invitation), diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index f3b34e1269..491302a225 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -642,7 +642,7 @@ class NotificationUtils @Inject constructor(private val context: Context, return NotificationCompat.Builder(context, channelID) .setOnlyAlertOnce(true) - .setContentTitle(stringProvider.getString(R.string.app_name)) + .setContentTitle(inviteNotifiableEvent.roomName ?: stringProvider.getString(R.string.app_name)) .setContentText(inviteNotifiableEvent.description) .setGroup(stringProvider.getString(R.string.app_name)) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) From 6cc6cc58f095f642a416bfb66df796494e759cb8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 12 Oct 2021 17:33:50 +0100 Subject: [PATCH 15/49] adding changelog entry --- changelog.d/582.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/582.feature diff --git a/changelog.d/582.feature b/changelog.d/582.feature new file mode 100644 index 0000000000..5f82e1b82c --- /dev/null +++ b/changelog.d/582.feature @@ -0,0 +1 @@ +Adding the room name to the invitation notification (if the room summary is available) \ No newline at end of file From 4459aab558b16c653d575fa186c6c961fc5e37e9 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 5 Oct 2021 16:55:28 +0100 Subject: [PATCH 16/49] making the event body non null and immutable to allow less cases to be handled - also puts in the basis for a separate notification refreshing implementation --- .../NotificationDrawerManager.kt | 511 +++++++++--------- 1 file changed, 258 insertions(+), 253 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 8c0e5fee5f..843b7208fd 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -250,31 +250,35 @@ class NotificationDrawerManager @Inject constructor(private val context: Context @WorkerThread private fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") - val session = currentSession ?: return val user = session.getUser(session.myUserId) // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash val myUserDisplayName = user?.toMatrixItem()?.getBestName() ?: session.myUserId val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE) + synchronized(eventList) { - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ") - // TMP code - var hasNewEvent = false - var summaryIsNoisy = false - val summaryInboxStyle = NotificationCompat.InboxStyle() + val useSplitNotifications = false + if (useSplitNotifications) { + // TODO + } else { + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ") + // TMP code + var hasNewEvent = false + var summaryIsNoisy = false + val summaryInboxStyle = NotificationCompat.InboxStyle() - // group events by room to create a single MessagingStyle notif - val roomIdToEventMap: MutableMap> = LinkedHashMap() - val simpleEvents: MutableList = ArrayList() - val invitationEvents: MutableList = ArrayList() + // group events by room to create a single MessagingStyle notif + val roomIdToEventMap: MutableMap> = LinkedHashMap() + val simpleEvents: MutableList = ArrayList() + val invitationEvents: MutableList = ArrayList() - val eventIterator = eventList.listIterator() - while (eventIterator.hasNext()) { - when (val event = eventIterator.next()) { - is NotifiableMessageEvent -> { - val roomId = event.roomId - val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } + val eventIterator = eventList.listIterator() + while (eventIterator.hasNext()) { + when (val event = eventIterator.next()) { + is NotifiableMessageEvent -> { + val roomId = event.roomId + val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { // forget this event @@ -296,59 +300,59 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups") + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups") - var globalLastMessageTimestamp = 0L + var globalLastMessageTimestamp = 0L - val newSettings = vectorPreferences.useCompleteNotificationFormat() - if (newSettings != useCompleteNotificationFormat) { - // Settings has changed, remove all current notifications - notificationUtils.cancelAllNotifications() - useCompleteNotificationFormat = newSettings - } - - var simpleNotificationRoomCounter = 0 - var simpleNotificationMessageCounter = 0 - - // events have been grouped by roomId - for ((roomId, events) in roomIdToEventMap) { - // Build the notification for the room - if (events.isEmpty() || events.all { it.isRedacted }) { - // Just clear this notification - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") - notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) - continue + val newSettings = vectorPreferences.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationUtils.cancelAllNotifications() + useCompleteNotificationFormat = newSettings } - simpleNotificationRoomCounter++ - val roomName = events[0].roomName ?: events[0].senderName ?: "" + var simpleNotificationRoomCounter = 0 + var simpleNotificationMessageCounter = 0 - val roomEventGroupInfo = RoomEventGroupInfo( - roomId = roomId, - isDirect = events[0].roomIsDirect, - roomDisplayName = roomName) - - val style = NotificationCompat.MessagingStyle(Person.Builder() - .setName(myUserDisplayName) - .setIcon(iconLoader.getUserIcon(myUserAvatarUrl)) - .setKey(events[0].matrixID) - .build()) - - style.isGroupConversation = !roomEventGroupInfo.isDirect - - if (!roomEventGroupInfo.isDirect) { - style.conversationTitle = roomEventGroupInfo.roomDisplayName - } - - val largeBitmap = getRoomBitmap(events) - - for (event in events) { - // if all events in this room have already been displayed there is no need to update it - if (!event.hasBeenDisplayed && !event.isRedacted) { - roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy - roomEventGroupInfo.customSound = event.soundName + // events have been grouped by roomId + for ((roomId, events) in roomIdToEventMap) { + // Build the notification for the room + if (events.isEmpty() || events.all { it.isRedacted }) { + // Just clear this notification + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") + notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) + continue } - roomEventGroupInfo.hasNewEvent = roomEventGroupInfo.hasNewEvent || !event.hasBeenDisplayed + + simpleNotificationRoomCounter++ + val roomName = events[0].roomName ?: events[0].senderName ?: "" + + val roomEventGroupInfo = RoomEventGroupInfo( + roomId = roomId, + isDirect = events[0].roomIsDirect, + roomDisplayName = roomName) + + val style = NotificationCompat.MessagingStyle(Person.Builder() + .setName(myUserDisplayName) + .setIcon(iconLoader.getUserIcon(myUserAvatarUrl)) + .setKey(events[0].matrixID) + .build()) + + style.isGroupConversation = !roomEventGroupInfo.isDirect + + if (!roomEventGroupInfo.isDirect) { + style.conversationTitle = roomEventGroupInfo.roomDisplayName + } + + val largeBitmap = getRoomBitmap(events) + + for (event in events) { + // if all events in this room have already been displayed there is no need to update it + if (!event.hasBeenDisplayed && !event.isRedacted) { + roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy + roomEventGroupInfo.customSound = event.soundName + } + roomEventGroupInfo.hasNewEvent = roomEventGroupInfo.hasNewEvent || !event.hasBeenDisplayed val senderPerson = if (event.outGoingMessage) { null @@ -373,211 +377,211 @@ class NotificationDrawerManager @Inject constructor(private val context: Context ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) } - if (event.outGoingMessage && event.outGoingMessageFailed) { - style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) - roomEventGroupInfo.hasSmartReplyError = true - } else { - if (!event.isRedacted) { - simpleNotificationMessageCounter++ - style.addMessage(event.body, event.timestamp, senderPerson) - } - } - event.hasBeenDisplayed = true // we can consider it as displayed - - // It is possible that this event was previously shown as an 'anonymous' simple notif. - // And now it will be merged in a single MessageStyle notif, so we can clean to be sure - notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID) - } - - try { - if (events.size == 1) { - val event = events[0] - if (roomEventGroupInfo.isDirect) { - val line = span { - span { - textStyle = "bold" - +String.format("%s: ", event.senderName) - } - +(event.description) - } - summaryInboxStyle.addLine(line) + if (event.outGoingMessage && event.outGoingMessageFailed) { + style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) + roomEventGroupInfo.hasSmartReplyError = true } else { - val line = span { - span { - textStyle = "bold" - +String.format("%s: %s ", roomName, event.senderName) - } - +(event.description) + if (!event.isRedacted) { + simpleNotificationMessageCounter++ + style.addMessage(event.body, event.timestamp, senderPerson) } - summaryInboxStyle.addLine(line) } + event.hasBeenDisplayed = true // we can consider it as displayed + + // It is possible that this event was previously shown as an 'anonymous' simple notif. + // And now it will be merged in a single MessageStyle notif, so we can clean to be sure + notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID) + } + + try { + if (events.size == 1) { + val event = events[0] + if (roomEventGroupInfo.isDirect) { + val line = span { + span { + textStyle = "bold" + +String.format("%s: ", event.senderName) + } + +(event.description) + } + summaryInboxStyle.addLine(line) + } else { + val line = span { + span { + textStyle = "bold" + +String.format("%s: %s ", roomName, event.senderName) + } + +(event.description) + } + summaryInboxStyle.addLine(line) + } + } else { + val summaryLine = stringProvider.getQuantityString( + R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) + summaryInboxStyle.addLine(summaryLine) + } + } catch (e: Throwable) { + // String not found or bad format + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") + summaryInboxStyle.addLine(roomName) + } + + if (firstTime || roomEventGroupInfo.hasNewEvent) { + // Should update displayed notification + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh") + val lastMessageTimestamp = events.last().timestamp + + if (globalLastMessageTimestamp < lastMessageTimestamp) { + globalLastMessageTimestamp = lastMessageTimestamp + } + + val tickerText = if (roomEventGroupInfo.isDirect) { + stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + } else { + stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) + } + + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildMessagesListNotification( + style, + roomEventGroupInfo, + largeBitmap, + lastMessageTimestamp, + myUserDisplayName, + tickerText) + + // is there an id for this room? + notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) + } + + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing } else { - val summaryLine = stringProvider.getQuantityString( - R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) - summaryInboxStyle.addLine(summaryLine) + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date") } - } catch (e: Throwable) { - // String not found or bad format - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") - summaryInboxStyle.addLine(roomName) } - if (firstTime || roomEventGroupInfo.hasNewEvent) { - // Should update displayed notification - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh") - val lastMessageTimestamp = events.last().timestamp - - if (globalLastMessageTimestamp < lastMessageTimestamp) { - globalLastMessageTimestamp = lastMessageTimestamp + // Handle invitation events + for (event in invitationEvents) { + // We build a invitation notification + if (firstTime || !event.hasBeenDisplayed) { + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) + notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) + } + event.hasBeenDisplayed = true // we can consider it as displayed + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || event.noisy + summaryInboxStyle.addLine(event.description) } + } - val tickerText = if (roomEventGroupInfo.isDirect) { - stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + // Handle simple events + for (event in simpleEvents) { + // We build a simple notification + if (firstTime || !event.hasBeenDisplayed) { + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) + notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) + } + event.hasBeenDisplayed = true // we can consider it as displayed + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || event.noisy + summaryInboxStyle.addLine(event.description) + } + } + + // ======== Build summary notification ========= + // On Android 7.0 (API level 24) and higher, the system automatically builds a summary for + // your group using snippets of text from each notification. The user can expand this + // notification to see each separate notification. + // To support older versions, which cannot show a nested group of notifications, + // you must create an extra notification that acts as the summary. + // This appears as the only notification and the system hides all the others. + // So this summary should include a snippet from all the other notifications, + // which the user can tap to open your app. + // The behavior of the group summary may vary on some device types such as wearables. + // To ensure the best experience on all devices and versions, always include a group summary when you create a group + // https://developer.android.com/training/notify-user/group + + if (eventList.isEmpty() || eventList.all { it.isRedacted }) { + notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) + } else if (hasNewEvent) { + // FIXME roomIdToEventMap.size is not correct, this is the number of rooms + val nbEvents = roomIdToEventMap.size + simpleEvents.size + val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) + summaryInboxStyle.setBigContentTitle(sumTitle) + // TODO get latest event? + .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildSummaryListNotification( + summaryInboxStyle, + sumTitle, + noisy = hasNewEvent && summaryIsNoisy, + lastMessageTimestamp = globalLastMessageTimestamp) + + notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) } else { - stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) - } + // Add the simple events as message (?) + simpleNotificationMessageCounter += simpleEvents.size + val numberOfInvitations = invitationEvents.size - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildMessagesListNotification( - style, - roomEventGroupInfo, - largeBitmap, - lastMessageTimestamp, - myUserDisplayName, - tickerText) - - // is there an id for this room? - notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) - } - - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing - } else { - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date") - } - } - - // Handle invitation events - for (event in invitationEvents) { - // We build a invitation notification - if (firstTime || !event.hasBeenDisplayed) { - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) - } - event.hasBeenDisplayed = true // we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } - } - - // Handle simple events - for (event in simpleEvents) { - // We build a simple notification - if (firstTime || !event.hasBeenDisplayed) { - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) - } - event.hasBeenDisplayed = true // we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } - } - - // ======== Build summary notification ========= - // On Android 7.0 (API level 24) and higher, the system automatically builds a summary for - // your group using snippets of text from each notification. The user can expand this - // notification to see each separate notification. - // To support older versions, which cannot show a nested group of notifications, - // you must create an extra notification that acts as the summary. - // This appears as the only notification and the system hides all the others. - // So this summary should include a snippet from all the other notifications, - // which the user can tap to open your app. - // The behavior of the group summary may vary on some device types such as wearables. - // To ensure the best experience on all devices and versions, always include a group summary when you create a group - // https://developer.android.com/training/notify-user/group - - if (eventList.isEmpty() || eventList.all { it.isRedacted }) { - notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) - } else if (hasNewEvent) { - // FIXME roomIdToEventMap.size is not correct, this is the number of rooms - val nbEvents = roomIdToEventMap.size + simpleEvents.size - val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) - summaryInboxStyle.setBigContentTitle(sumTitle) - // TODO get latest event? - .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) - - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildSummaryListNotification( - summaryInboxStyle, - sumTitle, - noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp) - - notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) - } else { - // Add the simple events as message (?) - simpleNotificationMessageCounter += simpleEvents.size - val numberOfInvitations = invitationEvents.size - - val privacyTitle = if (numberOfInvitations > 0) { - val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, numberOfInvitations, numberOfInvitations) - if (simpleNotificationMessageCounter > 0) { - // Invitation and message + val privacyTitle = if (numberOfInvitations > 0) { + val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, numberOfInvitations, numberOfInvitations) + if (simpleNotificationMessageCounter > 0) { + // Invitation and message + val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, + simpleNotificationMessageCounter, simpleNotificationMessageCounter) + if (simpleNotificationRoomCounter > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, + simpleNotificationRoomCounter, simpleNotificationRoomCounter) + stringProvider.getString( + R.string.notification_unread_notified_messages_in_room_and_invitation, + messageStr, + roomStr, + invitationsStr + ) + } else { + // In one room + stringProvider.getString( + R.string.notification_unread_notified_messages_and_invitation, + messageStr, + invitationsStr + ) + } + } else { + // Only invitation + invitationsStr + } + } else { + // No invitation, only messages val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, simpleNotificationMessageCounter, simpleNotificationMessageCounter) if (simpleNotificationRoomCounter > 1) { // In several rooms val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, simpleNotificationRoomCounter, simpleNotificationRoomCounter) - stringProvider.getString( - R.string.notification_unread_notified_messages_in_room_and_invitation, - messageStr, - roomStr, - invitationsStr - ) + stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) } else { // In one room - stringProvider.getString( - R.string.notification_unread_notified_messages_and_invitation, - messageStr, - invitationsStr - ) + messageStr } - } else { - // Only invitation - invitationsStr - } - } else { - // No invitation, only messages - val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, - simpleNotificationMessageCounter, simpleNotificationMessageCounter) - if (simpleNotificationRoomCounter > 1) { - // In several rooms - val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, - simpleNotificationRoomCounter, simpleNotificationRoomCounter) - stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) - } else { - // In one room - messageStr } + val notification = notificationUtils.buildSummaryListNotification( + style = null, + compatSummary = privacyTitle, + noisy = hasNewEvent && summaryIsNoisy, + lastMessageTimestamp = globalLastMessageTimestamp) + + notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) } - val notification = notificationUtils.buildSummaryListNotification( - style = null, - compatSummary = privacyTitle, - noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp) - notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) - } - - if (hasNewEvent && summaryIsNoisy) { - try { - // turn the screen on for 3 seconds - /* + if (hasNewEvent && summaryIsNoisy) { + try { + // turn the screen on for 3 seconds + /* TODO if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) { val pm = VectorApp.getInstance().getSystemService()!! @@ -587,13 +591,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context wl.release() } */ - } catch (e: Throwable) { - Timber.e(e, "## Failed to turn screen on") + } catch (e: Throwable) { + Timber.e(e, "## Failed to turn screen on") + } } } + // notice that we can get bit out of sync with actual display but not a big issue + firstTime = false } - // notice that we can get bit out of sync with actual display but not a big issue - firstTime = false } } @@ -657,10 +662,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } companion object { - private const val SUMMARY_NOTIFICATION_ID = 0 - private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 - private const val ROOM_EVENT_NOTIFICATION_ID = 2 - private const val ROOM_INVITATION_NOTIFICATION_ID = 3 + const val SUMMARY_NOTIFICATION_ID = 0 + const val ROOM_MESSAGES_NOTIFICATION_ID = 1 + const val ROOM_EVENT_NOTIFICATION_ID = 2 + const val ROOM_INVITATION_NOTIFICATION_ID = 3 // TODO Mutliaccount private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" From 7b0c4831345917975587752b2c62a31531d97e5e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Oct 2021 13:37:10 +0100 Subject: [PATCH 17/49] creating dedicated class for the processing the serialized events - updates the logic to track when events are removed as a way for the notifications to remove themselves, null events mean they've been removed --- .../notifications/NotifiableEventProcessor.kt | 73 +++++++ .../NotifiableEventProcessorTest.kt | 187 ++++++++++++++++++ .../app/test/fakes/FakeAutoAcceptInvites.kt | 27 +++ .../test/fakes/FakeOutdatedEventDetector.kt | 34 ++++ 4 files changed, 321 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt create mode 100644 vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeAutoAcceptInvites.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeOutdatedEventDetector.kt diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt new file mode 100644 index 0000000000..3f77ce54ca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import im.vector.app.features.invite.AutoAcceptInvites +import timber.log.Timber +import javax.inject.Inject + +class NotifiableEventProcessor @Inject constructor( + private val outdatedDetector: OutdatedEventDetector, + private val autoAcceptInvites: AutoAcceptInvites +) { + + fun modifyAndProcess(eventList: MutableList, currentRoomId: String?): ProcessedNotificationEvents { + val roomIdToEventMap: MutableMap> = LinkedHashMap() + val simpleEvents: MutableMap = LinkedHashMap() + val invitationEvents: MutableMap = LinkedHashMap() + + val eventIterator = eventList.listIterator() + while (eventIterator.hasNext()) { + when (val event = eventIterator.next()) { + is NotifiableMessageEvent -> { + val roomId = event.roomId + val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } + + // should we limit to last 7 messages per room? + if (shouldIgnoreMessageEventInRoom(currentRoomId, roomId) || outdatedDetector.isMessageOutdated(event)) { + // forget this event + eventIterator.remove() + } else { + roomEvents.add(event) + } + } + is InviteNotifiableEvent -> { + if (autoAcceptInvites.hideInvites) { + // Forget this event + eventIterator.remove() + invitationEvents[event.roomId] = null + } else { + invitationEvents[event.roomId] = event + } + } + is SimpleNotifiableEvent -> simpleEvents[event.eventId] = event + else -> Timber.w("Type not handled") + } + } + return ProcessedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) + } + + private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean { + return currentRoomId != null && roomId == currentRoomId + } +} + +data class ProcessedNotificationEvents( + val roomEvents: Map>, + val simpleEvents: Map, + val invitationEvents: Map +) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt new file mode 100644 index 0000000000..a5bb9978dd --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import im.vector.app.test.fakes.FakeAutoAcceptInvites +import im.vector.app.test.fakes.FakeOutdatedEventDetector +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private val NOT_VIEWING_A_ROOM: String? = null + +class NotifiableEventProcessorTest { + + private val outdatedDetector = FakeOutdatedEventDetector() + private val autoAcceptInvites = FakeAutoAcceptInvites() + + private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance, autoAcceptInvites) + + @Test + fun `given simple events when processing then return without mutating`() { + val (events, originalEvents) = createEventsList( + aSimpleNotifiableEvent(eventId = "event-1"), + aSimpleNotifiableEvent(eventId = "event-2") + ) + + val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + + result shouldBeEqualTo aProcessedNotificationEvents( + simpleEvents = mapOf( + "event-1" to events[0] as SimpleNotifiableEvent, + "event-2" to events[1] as SimpleNotifiableEvent + ) + ) + events shouldBeEqualTo originalEvents + } + + @Test + fun `given invites are auto accepted when processing then remove invitations`() { + autoAcceptInvites._isEnabled = true + val events = mutableListOf( + anInviteNotifiableEvent(roomId = "room-1"), + anInviteNotifiableEvent(roomId = "room-2") + ) + + val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + + result shouldBeEqualTo aProcessedNotificationEvents( + invitationEvents = mapOf( + "room-1" to null, + "room-2" to null + ) + ) + events shouldBeEqualTo emptyList() + } + + @Test + fun `given invites are not auto accepted when processing then return without mutating`() { + autoAcceptInvites._isEnabled = false + val (events, originalEvents) = createEventsList( + anInviteNotifiableEvent(roomId = "room-1"), + anInviteNotifiableEvent(roomId = "room-2") + ) + + val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + + result shouldBeEqualTo aProcessedNotificationEvents( + invitationEvents = mapOf( + "room-1" to originalEvents[0] as InviteNotifiableEvent, + "room-2" to originalEvents[1] as InviteNotifiableEvent + ) + ) + events shouldBeEqualTo originalEvents + } + + @Test + fun `given out of date message event when processing then removes message`() { + val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) + outdatedDetector.givenEventIsOutOfDate(events[0]) + + val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + + result shouldBeEqualTo aProcessedNotificationEvents( + roomEvents = mapOf( + "room-1" to emptyList() + ) + ) + events shouldBeEqualTo emptyList() + } + + @Test + fun `given in date message event when processing then without mutating`() { + val (events, originalEvents) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) + outdatedDetector.givenEventIsInDate(events[0]) + + val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + + result shouldBeEqualTo aProcessedNotificationEvents( + roomEvents = mapOf( + "room-1" to listOf(events[0] as NotifiableMessageEvent) + ) + ) + events shouldBeEqualTo originalEvents + } + + @Test + fun `given viewing the same room as message event when processing then removes message`() { + val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) + + val result = eventProcessor.modifyAndProcess(events, currentRoomId = "room-1") + + result shouldBeEqualTo aProcessedNotificationEvents( + roomEvents = mapOf( + "room-1" to emptyList() + ) + ) + events shouldBeEqualTo emptyList() + } +} + +fun createEventsList(vararg event: NotifiableEvent): Pair, List> { + val mutableEvents = mutableListOf(*event) + val immutableEvents = mutableEvents.toList() + return mutableEvents to immutableEvents +} + +fun aProcessedNotificationEvents(simpleEvents: Map = emptyMap(), + invitationEvents: Map = emptyMap(), + roomEvents: Map> = emptyMap() +) = ProcessedNotificationEvents( + roomEvents = roomEvents, + simpleEvents = simpleEvents, + invitationEvents = invitationEvents, +) + +fun aSimpleNotifiableEvent(eventId: String) = SimpleNotifiableEvent( + matrixID = null, + eventId = eventId, + editedEventId = null, + noisy = false, + title = "title", + description = "description", + type = null, + timestamp = 0, + soundName = null, + isPushGatewayEvent = false +) + +fun anInviteNotifiableEvent(roomId: String) = InviteNotifiableEvent( + matrixID = null, + eventId = "event-id", + roomId = roomId, + editedEventId = null, + noisy = false, + title = "title", + description = "description", + type = null, + timestamp = 0, + soundName = null, + isPushGatewayEvent = false +) + +fun aNotifiableMessageEvent(eventId: String, roomId: String) = NotifiableMessageEvent( + eventId = eventId, + editedEventId = null, + noisy = false, + timestamp = 0, + senderName = "sender-name", + senderId = "sending-id", + body = "message-body", + roomId = roomId, + roomName = "room-name", + roomIsDirect = false +) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAutoAcceptInvites.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAutoAcceptInvites.kt new file mode 100644 index 0000000000..778c2f113d --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAutoAcceptInvites.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import im.vector.app.features.invite.AutoAcceptInvites + +class FakeAutoAcceptInvites : AutoAcceptInvites { + + var _isEnabled: Boolean = false + + override val isEnabled: Boolean + get() = _isEnabled +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeOutdatedEventDetector.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeOutdatedEventDetector.kt new file mode 100644 index 0000000000..0e1d617ca2 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeOutdatedEventDetector.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import im.vector.app.features.notifications.NotifiableEvent +import im.vector.app.features.notifications.OutdatedEventDetector +import io.mockk.every +import io.mockk.mockk + +class FakeOutdatedEventDetector { + val instance = mockk() + + fun givenEventIsOutOfDate(notifiableEvent: NotifiableEvent) { + every { instance.isMessageOutdated(notifiableEvent) } returns true + } + + fun givenEventIsInDate(notifiableEvent: NotifiableEvent) { + every { instance.isMessageOutdated(notifiableEvent) } returns false + } +} From 0f4ec65b7a501f4f0138f737fbee99f6896f0d32 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Oct 2021 19:03:22 +0100 Subject: [PATCH 18/49] creating the notifications separate to where they're displayed - also handles when the event diff means the notifications should be removed --- .../notifications/NotificationFactory.kt | 106 +++++++++++++ .../notifications/RoomGroupMessageCreator.kt | 148 ++++++++++++++++++ .../SummaryGroupMessageCreator.kt | 140 +++++++++++++++++ .../notifications/NotificationFactoryTest.kt | 138 ++++++++++++++++ .../app/test/fakes/FakeNotificationUtils.kt | 41 +++++ .../test/fakes/FakeRoomGroupMessageCreator.kt | 37 +++++ .../fakes/FakeSummaryGroupMessageCreator.kt | 25 +++ 7 files changed, 635 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt create mode 100644 vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt create mode 100644 vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt create mode 100644 vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeNotificationUtils.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeRoomGroupMessageCreator.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSummaryGroupMessageCreator.kt diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt new file mode 100644 index 0000000000..174a457334 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import android.app.Notification +import javax.inject.Inject + +class NotificationFactory @Inject constructor( + private val notificationUtils: NotificationUtils, + private val roomGroupMessageCreator: RoomGroupMessageCreator, + private val summaryGroupMessageCreator: SummaryGroupMessageCreator +) { + + fun Map>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { + return this.map { (roomId, events) -> + when { + events.hasNoEventsToDisplay() -> RoomNotification.EmptyRoom(roomId) + else -> roomGroupMessageCreator.createRoomMessage(events, roomId, myUserDisplayName, myUserAvatarUrl) + } + } + } + + private fun List.hasNoEventsToDisplay() = isEmpty() || all { it.canNotBeDisplayed() } + + private fun NotifiableMessageEvent.canNotBeDisplayed() = hasBeenDisplayed || isRedacted + + fun Map.toNotifications(myUserId: String): List { + return this.map { (roomId, event) -> + when (event) { + null -> OneShotNotification.Removed(key = roomId) + else -> OneShotNotification.Append( + notificationUtils.buildRoomInvitationNotification(event, myUserId), + OneShotNotification.Append.Meta(key = roomId, summaryLine = event.description, isNoisy = event.noisy) + ) + } + } + } + + @JvmName("toNotificationsSimpleNotifiableEvent") + fun Map.toNotifications(myUserId: String): List { + return this.map { (eventId, event) -> + when (event) { + null -> OneShotNotification.Removed(key = eventId) + else -> OneShotNotification.Append( + notificationUtils.buildSimpleEventNotification(event, myUserId), + OneShotNotification.Append.Meta(key = eventId, summaryLine = event.description, isNoisy = event.noisy) + ) + } + } + } + + fun createSummaryNotification(roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + useCompleteNotificationFormat: Boolean): Notification { + return summaryGroupMessageCreator.createSummaryNotification( + roomNotifications = roomNotifications.mapToMeta(), + invitationNotifications = invitationNotifications.mapToMeta(), + simpleNotifications = simpleNotifications.mapToMeta(), + useCompleteNotificationFormat = useCompleteNotificationFormat + ) + } +} + +private fun List.mapToMeta() = filterIsInstance().map { it.meta } + +@JvmName("mapToMetaOneShotNotification") +private fun List.mapToMeta() = filterIsInstance().map { it.meta } + +sealed interface RoomNotification { + data class EmptyRoom(val roomId: String) : RoomNotification + data class Message(val notification: Notification, val meta: Meta) : RoomNotification { + data class Meta( + val summaryLine: CharSequence, + val messageCount: Int, + val latestTimestamp: Long, + val roomId: String, + val shouldBing: Boolean + ) + } +} + +sealed interface OneShotNotification { + data class Removed(val key: String) : OneShotNotification + data class Append(val notification: Notification, val meta: Meta) : OneShotNotification { + data class Meta( + val key: String, + val summaryLine: CharSequence, + val isNoisy: Boolean + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt new file mode 100644 index 0000000000..786ce40046 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import android.graphics.Bitmap +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import me.gujun.android.span.Span +import me.gujun.android.span.span +import timber.log.Timber +import javax.inject.Inject + +class RoomGroupMessageCreator @Inject constructor( + private val iconLoader: IconLoader, + private val bitmapLoader: BitmapLoader, + private val stringProvider: StringProvider, + private val notificationUtils: NotificationUtils +) { + + fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { + val firstKnownRoomEvent = events[0] + val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: "" + val roomIsGroup = !firstKnownRoomEvent.roomIsDirect + val style = NotificationCompat.MessagingStyle(Person.Builder() + .setName(userDisplayName) + .setIcon(iconLoader.getUserIcon(userAvatarUrl)) + .setKey(firstKnownRoomEvent.matrixID) + .build() + ).also { + it.conversationTitle = roomName.takeIf { roomIsGroup } + it.isGroupConversation = roomIsGroup + it.addMessagesFromEvents(events) + } + + val tickerText = if (roomIsGroup) { + stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) + } else { + stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + } + + val lastMessageTimestamp = events.last().timestamp + val smartReplyErrors = events.filter { it.isSmartReplyError() } + val messageCount = (events.size - smartReplyErrors.size) + val meta = RoomNotification.Message.Meta( + summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup), + messageCount = messageCount, + latestTimestamp = lastMessageTimestamp, + roomId = roomId, + shouldBing = events.any { it.noisy } + ) + return RoomNotification.Message( + notificationUtils.buildMessagesListNotification( + style, + RoomEventGroupInfo(roomId, roomName, isDirect = !roomIsGroup).also { + it.hasSmartReplyError = smartReplyErrors.isNotEmpty() + it.shouldBing = meta.shouldBing + it.customSound = events.last().soundName + }, + largeIcon = getRoomBitmap(events), + lastMessageTimestamp, + userDisplayName, + tickerText + ), + meta + ) + } + + private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { + events.forEach { event -> + val senderPerson = Person.Builder() + .setName(event.senderName) + .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) + .setKey(event.senderId) + .build() + when { + event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) + else -> addMessage(event.body, event.timestamp, senderPerson) + } + } + } + + private fun createRoomMessagesGroupSummaryLine(events: List, roomName: String, roomIsDirect: Boolean): CharSequence { + return try { + when (events.size) { + 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) + else -> { + stringProvider.getQuantityString( + R.plurals.notification_compat_summary_line_for_room, + events.size, + roomName, + events.size + ) + } + } + } catch (e: Throwable) { + // String not found or bad format + Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") + roomName + } + } + + private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): Span { + return if (roomIsDirect) { + span { + span { + textStyle = "bold" + +String.format("%s: ", event.senderName) + } + +(event.description) + } + } else { + span { + span { + textStyle = "bold" + +String.format("%s: %s ", roomName, event.senderName) + } + +(event.description) + } + } + } + + private fun getRoomBitmap(events: List): Bitmap? { + if (events.isEmpty()) return null + + // Use the last event (most recent?) + val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath + + return bitmapLoader.getRoomBitmap(roomAvatarPath) + } +} + +private fun NotifiableMessageEvent.isSmartReplyError() = this.outGoingMessage && this.outGoingMessageFailed diff --git a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt new file mode 100644 index 0000000000..38eac8b565 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import android.app.Notification +import androidx.core.app.NotificationCompat +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class SummaryGroupMessageCreator @Inject constructor( + private val stringProvider: StringProvider, + private val notificationUtils: NotificationUtils +) { + + /** + * ======== Build summary notification ========= + * On Android 7.0 (API level 24) and higher, the system automatically builds a summary for + * your group using snippets of text from each notification. The user can expand this + * notification to see each separate notification. + * To support older versions, which cannot show a nested group of notifications, + * you must create an extra notification that acts as the summary. + * This appears as the only notification and the system hides all the others. + * So this summary should include a snippet from all the other notifications, + * which the user can tap to open your app. + * The behavior of the group summary may vary on some device types such as wearables. + * To ensure the best experience on all devices and versions, always include a group summary when you create a group + * https://developer.android.com/training/notify-user/group + */ + fun createSummaryNotification(roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + useCompleteNotificationFormat: Boolean): Notification { + val summaryInboxStyle = NotificationCompat.InboxStyle().also { style -> + roomNotifications.forEach { style.addLine(it.summaryLine) } + invitationNotifications.forEach { style.addLine(it.summaryLine) } + simpleNotifications.forEach { style.addLine(it.summaryLine) } + } + + val summaryIsNoisy = roomNotifications.any { it.shouldBing } + || invitationNotifications.any { it.isNoisy } + || simpleNotifications.any { it.isNoisy } + + val messageCount = roomNotifications.fold(initial = 0) { acc, current -> acc + current.messageCount } + + val lastMessageTimestamp1 = roomNotifications.last().latestTimestamp + + // FIXME roomIdToEventMap.size is not correct, this is the number of rooms + val nbEvents = roomNotifications.size + simpleNotifications.size + val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) + summaryInboxStyle.setBigContentTitle(sumTitle) + // TODO get latest event? + .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + return if (useCompleteNotificationFormat + ) { + notificationUtils.buildSummaryListNotification( + summaryInboxStyle, + sumTitle, + noisy = summaryIsNoisy, + lastMessageTimestamp = lastMessageTimestamp1 + ) + } else { + processSimpleGroupSummary(summaryIsNoisy, messageCount, + simpleNotifications.size, invitationNotifications.size, + roomNotifications.size, lastMessageTimestamp1) + } + } + + private fun processSimpleGroupSummary(summaryIsNoisy: Boolean, + messageEventsCount: Int, + simpleEventsCount: Int, + invitationEventsCount: Int, + roomCount: Int, + lastMessageTimestamp: Long): Notification { + // Add the simple events as message (?) + val messageNotificationCount = messageEventsCount + simpleEventsCount + + val privacyTitle = if (invitationEventsCount > 0) { + val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, invitationEventsCount, invitationEventsCount) + if (messageNotificationCount > 0) { + // Invitation and message + val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount) + if (roomCount > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, + roomCount, roomCount) + stringProvider.getString( + R.string.notification_unread_notified_messages_in_room_and_invitation, + messageStr, + roomStr, + invitationsStr + ) + } else { + // In one room + stringProvider.getString( + R.string.notification_unread_notified_messages_and_invitation, + messageStr, + invitationsStr + ) + } + } else { + // Only invitation + invitationsStr + } + } else { + // No invitation, only messages + val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, + messageNotificationCount, messageNotificationCount) + if (roomCount > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, roomCount, roomCount) + stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) + } else { + // In one room + messageStr + } + } + return notificationUtils.buildSummaryListNotification( + style = null, + compatSummary = privacyTitle, + noisy = summaryIsNoisy, + lastMessageTimestamp = lastMessageTimestamp + ) + } +} diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt new file mode 100644 index 0000000000..c42e0b21c1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import im.vector.app.test.fakes.FakeNotificationUtils +import im.vector.app.test.fakes.FakeRoomGroupMessageCreator +import im.vector.app.test.fakes.FakeSummaryGroupMessageCreator +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private const val MY_USER_ID = "user-id" +private const val A_ROOM_ID = "room-id" +private const val AN_EVENT_ID = "event-id" + +private val MY_AVATAR_URL: String? = null +private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID) +private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID) +private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) + +class NotificationFactoryTest { + + private val notificationUtils = FakeNotificationUtils() + private val roomGroupMessageCreator = FakeRoomGroupMessageCreator() + private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator() + + private val notificationFactory = NotificationFactory( + notificationUtils.instance, + roomGroupMessageCreator.instance, + summaryGroupMessageCreator.instance + ) + + @Test + fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) { + val expectedNotification = notificationUtils.givenBuildRoomInvitationNotificationFor(AN_INVITATION_EVENT, MY_USER_ID) + val roomInvitation = mapOf(A_ROOM_ID to AN_INVITATION_EVENT) + + val result = roomInvitation.toNotifications(MY_USER_ID) + + result shouldBeEqualTo listOf(OneShotNotification.Append( + notification = expectedNotification, + meta = OneShotNotification.Append.Meta( + key = A_ROOM_ID, + summaryLine = AN_INVITATION_EVENT.description, + isNoisy = AN_INVITATION_EVENT.noisy + )) + ) + } + + @Test + fun `given a missing event in room invitation when mapping to notification then is Removed`() = testWith(notificationFactory) { + val missingEventRoomInvitation: Map = mapOf(A_ROOM_ID to null) + + val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) + + result shouldBeEqualTo listOf(OneShotNotification.Removed( + key = A_ROOM_ID + )) + } + + @Test + fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) { + val expectedNotification = notificationUtils.givenBuildSimpleInvitationNotificationFor(A_SIMPLE_EVENT, MY_USER_ID) + val roomInvitation = mapOf(AN_EVENT_ID to A_SIMPLE_EVENT) + + val result = roomInvitation.toNotifications(MY_USER_ID) + + result shouldBeEqualTo listOf(OneShotNotification.Append( + notification = expectedNotification, + meta = OneShotNotification.Append.Meta( + key = AN_EVENT_ID, + summaryLine = A_SIMPLE_EVENT.description, + isNoisy = A_SIMPLE_EVENT.noisy + )) + ) + } + + @Test + fun `given a missing simple event when mapping to notification then is Removed`() = testWith(notificationFactory) { + val missingEventRoomInvitation: Map = mapOf(AN_EVENT_ID to null) + + val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) + + result shouldBeEqualTo listOf(OneShotNotification.Removed( + key = AN_EVENT_ID + )) + } + + @Test + fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) { + val events = listOf(A_MESSAGE_EVENT) + val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(events, A_ROOM_ID, MY_USER_ID, MY_AVATAR_URL) + val roomWithMessage = mapOf(A_ROOM_ID to events) + + val result = roomWithMessage.toNotifications(MY_USER_ID, MY_AVATAR_URL) + + result shouldBeEqualTo listOf(expectedNotification) + } + + @Test + fun `given a room with no events to display when mapping to notification then is Empty`() = testWith(notificationFactory) { + val emptyRoom: Map> = mapOf(A_ROOM_ID to emptyList()) + + val result = emptyRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) + + result shouldBeEqualTo listOf(RoomNotification.EmptyRoom( + roomId = A_ROOM_ID + )) + } + + @Test + fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { + val redactedRoom = mapOf(A_ROOM_ID to listOf(A_MESSAGE_EVENT.copy().apply { isRedacted = true })) + + val result = redactedRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) + + result shouldBeEqualTo listOf(RoomNotification.EmptyRoom( + roomId = A_ROOM_ID + )) + } +} + +fun testWith(receiver: T, block: T.() -> Unit) { + receiver.block() +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationUtils.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationUtils.kt new file mode 100644 index 0000000000..39f2ad59ff --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationUtils.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import android.app.Notification +import im.vector.app.features.notifications.InviteNotifiableEvent +import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.notifications.SimpleNotifiableEvent +import io.mockk.every +import io.mockk.mockk + +class FakeNotificationUtils { + + val instance = mockk() + + fun givenBuildRoomInvitationNotificationFor(event: InviteNotifiableEvent, myUserId: String): Notification { + val mockNotification = mockk() + every { instance.buildRoomInvitationNotification(event, myUserId) } returns mockNotification + return mockNotification + } + + fun givenBuildSimpleInvitationNotificationFor(event: SimpleNotifiableEvent, myUserId: String): Notification { + val mockNotification = mockk() + every { instance.buildSimpleEventNotification(event, myUserId) } returns mockNotification + return mockNotification + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomGroupMessageCreator.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomGroupMessageCreator.kt new file mode 100644 index 0000000000..c164b9a661 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomGroupMessageCreator.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import im.vector.app.features.notifications.NotifiableMessageEvent +import im.vector.app.features.notifications.RoomGroupMessageCreator +import im.vector.app.features.notifications.RoomNotification +import io.mockk.every +import io.mockk.mockk + +class FakeRoomGroupMessageCreator { + + val instance = mockk() + + fun givenCreatesRoomMessageFor(events: List, + roomId: String, + userDisplayName: String, + userAvatarUrl: String?): RoomNotification.Message { + val mockMessage = mockk() + every { instance.createRoomMessage(events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage + return mockMessage + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSummaryGroupMessageCreator.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSummaryGroupMessageCreator.kt new file mode 100644 index 0000000000..eef77298a0 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSummaryGroupMessageCreator.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import im.vector.app.features.notifications.SummaryGroupMessageCreator +import io.mockk.mockk + +class FakeSummaryGroupMessageCreator { + + val instance = mockk() +} From 3023cb4d39a819c4c85ad93da0d9fc7542f2c5e8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 6 Oct 2021 20:28:29 +0100 Subject: [PATCH 19/49] chaining the event process, notification creation and display logic into a NotificationRender - extract the displaying into its own class to avoid leaking the entire notificationutils - cancel/display notification actions are completely driven by the event or abscense of event from the eventList - attempts to avoid redundant render passes by checking if the eventList has changed since the last render --- .../notifications/NotificationDisplayer.kt | 45 +++++ .../notifications/NotificationFactory.kt | 4 +- .../notifications/NotificationRenderer.kt | 101 ++++++++++ .../SummaryGroupMessageCreator.kt | 28 +-- .../notifications/NotificationFactoryTest.kt | 4 +- .../notifications/NotificationRendererTest.kt | 183 ++++++++++++++++++ .../fakes/FakeNotifiableEventProcessor.kt | 32 +++ .../test/fakes/FakeNotificationDisplayer.kt | 42 ++++ .../app/test/fakes/FakeNotificationFactory.kt | 56 ++++++ .../app/test/fakes/FakeVectorPreferences.kt | 30 +++ 10 files changed, 507 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt create mode 100644 vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt create mode 100644 vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeNotificationDisplayer.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt new file mode 100644 index 0000000000..680ff32a52 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import android.app.Notification +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import timber.log.Timber +import javax.inject.Inject + +class NotificationDisplayer @Inject constructor(context: Context) { + + private val notificationManager = NotificationManagerCompat.from(context) + + fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + notificationManager.notify(tag, id, notification) + } + + fun cancelNotificationMessage(tag: String?, id: Int) { + notificationManager.cancel(tag, id) + } + + fun cancelAllNotifications() { + // Keep this try catch (reported by GA) + try { + notificationManager.cancelAll() + } catch (e: Exception) { + Timber.e(e, "## cancelAllNotifications() failed") + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 174a457334..55e9f7352d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -28,7 +28,7 @@ class NotificationFactory @Inject constructor( fun Map>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { return this.map { (roomId, events) -> when { - events.hasNoEventsToDisplay() -> RoomNotification.EmptyRoom(roomId) + events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) else -> roomGroupMessageCreator.createRoomMessage(events, roomId, myUserDisplayName, myUserAvatarUrl) } } @@ -82,7 +82,7 @@ private fun List.mapToMeta() = filterIsInstance.mapToMeta() = filterIsInstance().map { it.meta } sealed interface RoomNotification { - data class EmptyRoom(val roomId: String) : RoomNotification + data class Removed(val roomId: String) : RoomNotification data class Message(val notification: Notification, val meta: Meta) : RoomNotification { data class Meta( val summaryLine: CharSequence, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt new file mode 100644 index 0000000000..257f998774 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 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 im.vector.app.features.notifications + +import androidx.annotation.WorkerThread +import im.vector.app.features.settings.VectorPreferences +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NotificationRenderer @Inject constructor(private val notifiableEventProcessor: NotifiableEventProcessor, + private val notificationDisplayer: NotificationDisplayer, + private val vectorPreferences: VectorPreferences, + private val notificationFactory: NotificationFactory) { + + private var lastKnownEventList = -1 + private var useCompleteNotificationFormat = vectorPreferences.useCompleteNotificationFormat() + + @WorkerThread + fun render(currentRoomId: String?, myUserId: String, myUserDisplayName: String, myUserAvatarUrl: String?, eventList: MutableList) { + Timber.v("refreshNotificationDrawerBg()") + val newSettings = vectorPreferences.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationDisplayer.cancelAllNotifications() + useCompleteNotificationFormat = newSettings + } + + val notificationEvents = notifiableEventProcessor.modifyAndProcess(eventList, currentRoomId) + if (lastKnownEventList == notificationEvents.hashCode()) { + Timber.d("Skipping notification update due to event list not changing") + } else { + processEvents(notificationEvents, myUserId, myUserDisplayName, myUserAvatarUrl) + lastKnownEventList = notificationEvents.hashCode() + } + } + + private fun processEvents(notificationEvents: ProcessedNotificationEvents, myUserId: String, myUserDisplayName: String, myUserAvatarUrl: String?) { + val (roomEvents, simpleEvents, invitationEvents) = notificationEvents + with(notificationFactory) { + val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) + val invitationNotifications = invitationEvents.toNotifications(myUserId) + val simpleNotifications = simpleEvents.toNotifications(myUserId) + + if (roomNotifications.isEmpty() && invitationNotifications.isEmpty() && simpleNotifications.isEmpty()) { + notificationDisplayer.cancelNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) + } else { + val summaryNotification = createSummaryNotification( + roomNotifications = roomNotifications, + invitationNotifications = invitationNotifications, + simpleNotifications = simpleNotifications, + useCompleteNotificationFormat = useCompleteNotificationFormat + ) + roomNotifications.forEach { wrapper -> + when (wrapper) { + is RoomNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) + is RoomNotification.Message -> if (useCompleteNotificationFormat) { + Timber.d("Updating room messages notification ${wrapper.meta.roomId}") + notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) + } + } + } + + invitationNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID) + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating invitation notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) + } + } + } + + simpleNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID) + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating simple notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) + } + } + } + notificationDisplayer.showNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, summaryNotification) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt index 38eac8b565..dc9ff92aa6 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt @@ -22,25 +22,25 @@ import im.vector.app.R import im.vector.app.core.resources.StringProvider import javax.inject.Inject +/** + * ======== Build summary notification ========= + * On Android 7.0 (API level 24) and higher, the system automatically builds a summary for + * your group using snippets of text from each notification. The user can expand this + * notification to see each separate notification. + * To support older versions, which cannot show a nested group of notifications, + * you must create an extra notification that acts as the summary. + * This appears as the only notification and the system hides all the others. + * So this summary should include a snippet from all the other notifications, + * which the user can tap to open your app. + * The behavior of the group summary may vary on some device types such as wearables. + * To ensure the best experience on all devices and versions, always include a group summary when you create a group + * https://developer.android.com/training/notify-user/group + */ class SummaryGroupMessageCreator @Inject constructor( private val stringProvider: StringProvider, private val notificationUtils: NotificationUtils ) { - /** - * ======== Build summary notification ========= - * On Android 7.0 (API level 24) and higher, the system automatically builds a summary for - * your group using snippets of text from each notification. The user can expand this - * notification to see each separate notification. - * To support older versions, which cannot show a nested group of notifications, - * you must create an extra notification that acts as the summary. - * This appears as the only notification and the system hides all the others. - * So this summary should include a snippet from all the other notifications, - * which the user can tap to open your app. - * The behavior of the group summary may vary on some device types such as wearables. - * To ensure the best experience on all devices and versions, always include a group summary when you create a group - * https://developer.android.com/training/notify-user/group - */ fun createSummaryNotification(roomNotifications: List, invitationNotifications: List, simpleNotifications: List, diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index c42e0b21c1..c08be9e8c7 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -116,7 +116,7 @@ class NotificationFactoryTest { val result = emptyRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) - result shouldBeEqualTo listOf(RoomNotification.EmptyRoom( + result shouldBeEqualTo listOf(RoomNotification.Removed( roomId = A_ROOM_ID )) } @@ -127,7 +127,7 @@ class NotificationFactoryTest { val result = redactedRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) - result shouldBeEqualTo listOf(RoomNotification.EmptyRoom( + result shouldBeEqualTo listOf(RoomNotification.Removed( roomId = A_ROOM_ID )) } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt new file mode 100644 index 0000000000..a07dd61368 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2021 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 im.vector.app.features.notifications + +import android.app.Notification +import im.vector.app.test.fakes.FakeNotifiableEventProcessor +import im.vector.app.test.fakes.FakeNotificationDisplayer +import im.vector.app.test.fakes.FakeNotificationFactory +import im.vector.app.test.fakes.FakeVectorPreferences +import io.mockk.mockk +import org.junit.Test + +private const val A_CURRENT_ROOM_ID = "current-room-id" +private const val MY_USER_ID = "my-user-id" +private const val MY_USER_DISPLAY_NAME = "display-name" +private const val MY_USER_AVATAR_URL = "avatar-url" +private const val AN_EVENT_ID = "event-id" +private const val A_ROOM_ID = "room-id" +private const val USE_COMPLETE_NOTIFICATION_FORMAT = true + +private val AN_EVENT_LIST = mutableListOf() +private val A_PROCESSED_EVENTS = ProcessedNotificationEvents(emptyMap(), emptyMap(), emptyMap()) +private val A_SUMMARY_NOTIFICATION = mockk() +private val A_NOTIFICATION = mockk() +private val MESSAGE_META = RoomNotification.Message.Meta( + summaryLine = "ignored", messageCount = 1, latestTimestamp = -1, roomId = A_ROOM_ID, shouldBing = false +) +private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false) + +class NotificationRendererTest { + + private val notifiableEventProcessor = FakeNotifiableEventProcessor() + private val notificationDisplayer = FakeNotificationDisplayer() + private val preferences = FakeVectorPreferences().also { + it.givenUseCompleteNotificationFormat(USE_COMPLETE_NOTIFICATION_FORMAT) + } + private val notificationFactory = FakeNotificationFactory() + + private val notificationRenderer = NotificationRenderer( + notifiableEventProcessor = notifiableEventProcessor.instance, + notificationDisplayer = notificationDisplayer.instance, + vectorPreferences = preferences.instance, + notificationFactory = notificationFactory.instance + ) + + @Test + fun `given no notifications when rendering then cancels summary notification`() { + givenNoNotifications() + + renderEventsAsNotifications() + + notificationDisplayer.verifySummaryCancelled() + notificationDisplayer.verifyNoOtherInteractions() + } + + @Test + fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() { + givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID))) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + } + } + + @Test + fun `given a room message group notification is added when rendering then show the message notification and update summary`() { + givenNotifications(roomNotifications = listOf(RoomNotification.Message( + A_NOTIFICATION, + MESSAGE_META + ))) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + showNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, A_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + } + } + + @Test + fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() { + givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID))) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + cancelNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + } + } + + @Test + fun `given a simple notification is added when rendering then show the simple notification and update summary`() { + givenNotifications(simpleNotifications = listOf(OneShotNotification.Append( + A_NOTIFICATION, + ONE_SHOT_META.copy(key = AN_EVENT_ID) + ))) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + showNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, A_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + } + } + + @Test + fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() { + givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID))) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + } + } + + @Test + fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() { + givenNotifications(simpleNotifications = listOf(OneShotNotification.Append( + A_NOTIFICATION, + ONE_SHOT_META.copy(key = A_ROOM_ID) + ))) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + showNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, A_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + } + } + + private fun renderEventsAsNotifications() { + notificationRenderer.render( + currentRoomId = A_CURRENT_ROOM_ID, + myUserId = MY_USER_ID, + myUserDisplayName = MY_USER_DISPLAY_NAME, + myUserAvatarUrl = MY_USER_AVATAR_URL, + eventList = AN_EVENT_LIST + ) + } + + private fun givenNoNotifications() { + givenNotifications(emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_SUMMARY_NOTIFICATION) + } + + private fun givenNotifications(roomNotifications: List = emptyList(), + invitationNotifications: List = emptyList(), + simpleNotifications: List = emptyList(), + useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT, + summaryNotification: Notification = A_SUMMARY_NOTIFICATION) { + notifiableEventProcessor.givenProcessedEventsFor(AN_EVENT_LIST, A_CURRENT_ROOM_ID, A_PROCESSED_EVENTS) + notificationFactory.givenNotificationsFor( + processedEvents = A_PROCESSED_EVENTS, + myUserId = MY_USER_ID, + myUserDisplayName = MY_USER_DISPLAY_NAME, + myUserAvatarUrl = MY_USER_AVATAR_URL, + useCompleteNotificationFormat = useCompleteNotificationFormat, + roomNotifications = roomNotifications, + invitationNotifications = invitationNotifications, + simpleNotifications = simpleNotifications, + summaryNotification = summaryNotification + ) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt new file mode 100644 index 0000000000..93f5e40524 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import im.vector.app.features.notifications.NotifiableEvent +import im.vector.app.features.notifications.NotifiableEventProcessor +import im.vector.app.features.notifications.ProcessedNotificationEvents +import io.mockk.every +import io.mockk.mockk + +class FakeNotifiableEventProcessor { + + val instance = mockk() + + fun givenProcessedEventsFor(events: MutableList, currentRoomId: String?, processedEvents: ProcessedNotificationEvents) { + every { instance.modifyAndProcess(events, currentRoomId) } returns processedEvents + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationDisplayer.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationDisplayer.kt new file mode 100644 index 0000000000..2856b0f49c --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationDisplayer.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import im.vector.app.features.notifications.NotificationDisplayer +import im.vector.app.features.notifications.NotificationDrawerManager +import io.mockk.confirmVerified +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyOrder + +class FakeNotificationDisplayer { + + val instance = mockk(relaxed = true) + + fun verifySummaryCancelled() { + verify { instance.cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) } + } + + fun verifyNoOtherInteractions() { + confirmVerified(instance) + } + + fun verifyInOrder(verifyBlock: NotificationDisplayer.() -> Unit) { + verifyOrder { verifyBlock(instance) } + verifyNoOtherInteractions() + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt new file mode 100644 index 0000000000..921999bd93 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import android.app.Notification +import im.vector.app.features.notifications.NotificationFactory +import im.vector.app.features.notifications.OneShotNotification +import im.vector.app.features.notifications.ProcessedNotificationEvents +import im.vector.app.features.notifications.RoomNotification +import io.mockk.every +import io.mockk.mockk + +class FakeNotificationFactory { + + val instance = mockk() + + fun givenNotificationsFor(processedEvents: ProcessedNotificationEvents, + myUserId: String, + myUserDisplayName: String, + myUserAvatarUrl: String?, + useCompleteNotificationFormat: Boolean, + roomNotifications: List, + invitationNotifications: List, + simpleNotifications: List, + summaryNotification: Notification) { + with(instance) { + every { processedEvents.roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) } returns roomNotifications + every { processedEvents.invitationEvents.toNotifications(myUserId) } returns invitationNotifications + every { processedEvents.simpleEvents.toNotifications(myUserId) } returns simpleNotifications + + every { + createSummaryNotification( + roomNotifications, + invitationNotifications, + simpleNotifications, + useCompleteNotificationFormat + ) + } returns summaryNotification + + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt new file mode 100644 index 0000000000..eb8f9ac413 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 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 im.vector.app.test.fakes + +import im.vector.app.features.settings.VectorPreferences +import io.mockk.every +import io.mockk.mockk + +class FakeVectorPreferences { + + val instance = mockk() + + fun givenUseCompleteNotificationFormat(value: Boolean) { + every { instance.useCompleteNotificationFormat() } returns value + } +} From c85afa96d340e21f3cc31e1d048279db5290a3ef Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 08:22:45 +0100 Subject: [PATCH 20/49] lifting settings change to cancel all notifications out of the renderer - the renderer's responsibility it handling events --- .../NotificationDrawerManager.kt | 376 +----------------- .../notifications/NotificationFactory.kt | 4 +- .../notifications/NotificationRenderer.kt | 33 +- .../notifications/RoomGroupMessageCreator.kt | 39 +- .../notifications/NotificationRendererTest.kt | 5 +- 5 files changed, 62 insertions(+), 395 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 843b7208fd..a3a3e898ba 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -16,26 +16,15 @@ package im.vector.app.features.notifications import android.content.Context -import android.graphics.Bitmap -import android.os.Build import android.os.Handler import android.os.HandlerThread import androidx.annotation.WorkerThread -import androidx.core.app.NotificationCompat -import androidx.core.app.Person -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig import im.vector.app.R -import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.FirstThrottler import im.vector.app.features.displayname.getBestName -import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.settings.VectorPreferences -import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.util.toMatrixItem @@ -54,12 +43,8 @@ import javax.inject.Singleton class NotificationDrawerManager @Inject constructor(private val context: Context, private val notificationUtils: NotificationUtils, private val vectorPreferences: VectorPreferences, - private val stringProvider: StringProvider, private val activeSessionDataSource: ActiveSessionDataSource, - private val iconLoader: IconLoader, - private val bitmapLoader: BitmapLoader, - private val outdatedDetector: OutdatedEventDetector?, - private val autoAcceptInvites: AutoAcceptInvites) { + private val notificationRenderer: NotificationRenderer) { private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY) private var backgroundHandler: Handler @@ -69,13 +54,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context backgroundHandler = Handler(handlerThread.looper) } - // The first time the notification drawer is refreshed, we force re-render of all notifications - private var firstTime = true - private val eventList = loadEventInfo() - private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) - private var currentRoomId: String? = null // TODO Multi-session: this will have to be improved @@ -258,359 +238,17 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE) synchronized(eventList) { - val useSplitNotifications = false - if (useSplitNotifications) { - // TODO - } else { - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ") - // TMP code - var hasNewEvent = false - var summaryIsNoisy = false - val summaryInboxStyle = NotificationCompat.InboxStyle() - - // group events by room to create a single MessagingStyle notif - val roomIdToEventMap: MutableMap> = LinkedHashMap() - val simpleEvents: MutableList = ArrayList() - val invitationEvents: MutableList = ArrayList() - - val eventIterator = eventList.listIterator() - while (eventIterator.hasNext()) { - when (val event = eventIterator.next()) { - is NotifiableMessageEvent -> { - val roomId = event.roomId - val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } - - if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { - // forget this event - eventIterator.remove() - } else { - roomEvents.add(event) - } - } - is InviteNotifiableEvent -> { - if (autoAcceptInvites.hideInvites) { - // Forget this event - eventIterator.remove() - } else { - invitationEvents.add(event) - } - } - is SimpleNotifiableEvent -> simpleEvents.add(event) - else -> Timber.w("Type not handled") - } + val newSettings = vectorPreferences.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationUtils.cancelAllNotifications() + useCompleteNotificationFormat = newSettings } - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups") - - var globalLastMessageTimestamp = 0L - - val newSettings = vectorPreferences.useCompleteNotificationFormat() - if (newSettings != useCompleteNotificationFormat) { - // Settings has changed, remove all current notifications - notificationUtils.cancelAllNotifications() - useCompleteNotificationFormat = newSettings - } - - var simpleNotificationRoomCounter = 0 - var simpleNotificationMessageCounter = 0 - - // events have been grouped by roomId - for ((roomId, events) in roomIdToEventMap) { - // Build the notification for the room - if (events.isEmpty() || events.all { it.isRedacted }) { - // Just clear this notification - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") - notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) - continue - } - - simpleNotificationRoomCounter++ - val roomName = events[0].roomName ?: events[0].senderName ?: "" - - val roomEventGroupInfo = RoomEventGroupInfo( - roomId = roomId, - isDirect = events[0].roomIsDirect, - roomDisplayName = roomName) - - val style = NotificationCompat.MessagingStyle(Person.Builder() - .setName(myUserDisplayName) - .setIcon(iconLoader.getUserIcon(myUserAvatarUrl)) - .setKey(events[0].matrixID) - .build()) - - style.isGroupConversation = !roomEventGroupInfo.isDirect - - if (!roomEventGroupInfo.isDirect) { - style.conversationTitle = roomEventGroupInfo.roomDisplayName - } - - val largeBitmap = getRoomBitmap(events) - - for (event in events) { - // if all events in this room have already been displayed there is no need to update it - if (!event.hasBeenDisplayed && !event.isRedacted) { - roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy - roomEventGroupInfo.customSound = event.soundName - } - roomEventGroupInfo.hasNewEvent = roomEventGroupInfo.hasNewEvent || !event.hasBeenDisplayed - - val senderPerson = if (event.outGoingMessage) { - null - } else { - Person.Builder() - .setName(event.senderName) - .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) - .setKey(event.senderId) - .build() - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val openRoomIntent = RoomDetailActivity.shortcutIntent(context, roomId) - - val shortcut = ShortcutInfoCompat.Builder(context, roomId) - .setLongLived(true) - .setIntent(openRoomIntent) - .setShortLabel(roomName) - .setIcon(largeBitmap?.let { IconCompat.createWithAdaptiveBitmap(it) } ?: iconLoader.getUserIcon(event.senderAvatarPath)) - .build() - - ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) - } - - if (event.outGoingMessage && event.outGoingMessageFailed) { - style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) - roomEventGroupInfo.hasSmartReplyError = true - } else { - if (!event.isRedacted) { - simpleNotificationMessageCounter++ - style.addMessage(event.body, event.timestamp, senderPerson) - } - } - event.hasBeenDisplayed = true // we can consider it as displayed - - // It is possible that this event was previously shown as an 'anonymous' simple notif. - // And now it will be merged in a single MessageStyle notif, so we can clean to be sure - notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID) - } - - try { - if (events.size == 1) { - val event = events[0] - if (roomEventGroupInfo.isDirect) { - val line = span { - span { - textStyle = "bold" - +String.format("%s: ", event.senderName) - } - +(event.description) - } - summaryInboxStyle.addLine(line) - } else { - val line = span { - span { - textStyle = "bold" - +String.format("%s: %s ", roomName, event.senderName) - } - +(event.description) - } - summaryInboxStyle.addLine(line) - } - } else { - val summaryLine = stringProvider.getQuantityString( - R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) - summaryInboxStyle.addLine(summaryLine) - } - } catch (e: Throwable) { - // String not found or bad format - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") - summaryInboxStyle.addLine(roomName) - } - - if (firstTime || roomEventGroupInfo.hasNewEvent) { - // Should update displayed notification - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh") - val lastMessageTimestamp = events.last().timestamp - - if (globalLastMessageTimestamp < lastMessageTimestamp) { - globalLastMessageTimestamp = lastMessageTimestamp - } - - val tickerText = if (roomEventGroupInfo.isDirect) { - stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) - } else { - stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) - } - - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildMessagesListNotification( - style, - roomEventGroupInfo, - largeBitmap, - lastMessageTimestamp, - myUserDisplayName, - tickerText) - - // is there an id for this room? - notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) - } - - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing - } else { - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date") - } - } - - // Handle invitation events - for (event in invitationEvents) { - // We build a invitation notification - if (firstTime || !event.hasBeenDisplayed) { - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) - } - event.hasBeenDisplayed = true // we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } - } - - // Handle simple events - for (event in simpleEvents) { - // We build a simple notification - if (firstTime || !event.hasBeenDisplayed) { - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) - } - event.hasBeenDisplayed = true // we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } - } - - // ======== Build summary notification ========= - // On Android 7.0 (API level 24) and higher, the system automatically builds a summary for - // your group using snippets of text from each notification. The user can expand this - // notification to see each separate notification. - // To support older versions, which cannot show a nested group of notifications, - // you must create an extra notification that acts as the summary. - // This appears as the only notification and the system hides all the others. - // So this summary should include a snippet from all the other notifications, - // which the user can tap to open your app. - // The behavior of the group summary may vary on some device types such as wearables. - // To ensure the best experience on all devices and versions, always include a group summary when you create a group - // https://developer.android.com/training/notify-user/group - - if (eventList.isEmpty() || eventList.all { it.isRedacted }) { - notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) - } else if (hasNewEvent) { - // FIXME roomIdToEventMap.size is not correct, this is the number of rooms - val nbEvents = roomIdToEventMap.size + simpleEvents.size - val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) - summaryInboxStyle.setBigContentTitle(sumTitle) - // TODO get latest event? - .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) - - if (useCompleteNotificationFormat) { - val notification = notificationUtils.buildSummaryListNotification( - summaryInboxStyle, - sumTitle, - noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp) - - notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) - } else { - // Add the simple events as message (?) - simpleNotificationMessageCounter += simpleEvents.size - val numberOfInvitations = invitationEvents.size - - val privacyTitle = if (numberOfInvitations > 0) { - val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, numberOfInvitations, numberOfInvitations) - if (simpleNotificationMessageCounter > 0) { - // Invitation and message - val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, - simpleNotificationMessageCounter, simpleNotificationMessageCounter) - if (simpleNotificationRoomCounter > 1) { - // In several rooms - val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, - simpleNotificationRoomCounter, simpleNotificationRoomCounter) - stringProvider.getString( - R.string.notification_unread_notified_messages_in_room_and_invitation, - messageStr, - roomStr, - invitationsStr - ) - } else { - // In one room - stringProvider.getString( - R.string.notification_unread_notified_messages_and_invitation, - messageStr, - invitationsStr - ) - } - } else { - // Only invitation - invitationsStr - } - } else { - // No invitation, only messages - val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, - simpleNotificationMessageCounter, simpleNotificationMessageCounter) - if (simpleNotificationRoomCounter > 1) { - // In several rooms - val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, - simpleNotificationRoomCounter, simpleNotificationRoomCounter) - stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) - } else { - // In one room - messageStr - } - } - val notification = notificationUtils.buildSummaryListNotification( - style = null, - compatSummary = privacyTitle, - noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp) - - notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) - } - - if (hasNewEvent && summaryIsNoisy) { - try { - // turn the screen on for 3 seconds - /* - TODO - if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) { - val pm = VectorApp.getInstance().getSystemService()!! - val wl = pm.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP, - NotificationDrawerManager::class.java.name) - wl.acquire(3000) - wl.release() - } - */ - } catch (e: Throwable) { - Timber.e(e, "## Failed to turn screen on") - } - } - } - // notice that we can get bit out of sync with actual display but not a big issue - firstTime = false - } + notificationRenderer.render(currentRoomId, session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventList) } } - private fun getRoomBitmap(events: List): Bitmap? { - if (events.isEmpty()) return null - - // Use the last event (most recent?) - val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath - - return bitmapLoader.getRoomBitmap(roomAvatarPath) - } - fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean { return currentRoomId != null && roomId == currentRoomId } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 55e9f7352d..ec8e372c4e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -17,6 +17,8 @@ package im.vector.app.features.notifications import android.app.Notification +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat import javax.inject.Inject class NotificationFactory @Inject constructor( @@ -83,7 +85,7 @@ private fun List.mapToMeta() = filterIsInstance) { + fun render(currentRoomId: String?, + myUserId: String, + myUserDisplayName: String, + myUserAvatarUrl: String?, + useCompleteNotificationFormat: Boolean, + eventList: MutableList) { Timber.v("refreshNotificationDrawerBg()") - val newSettings = vectorPreferences.useCompleteNotificationFormat() - if (newSettings != useCompleteNotificationFormat) { - // Settings has changed, remove all current notifications - notificationDisplayer.cancelAllNotifications() - useCompleteNotificationFormat = newSettings - } - val notificationEvents = notifiableEventProcessor.modifyAndProcess(eventList, currentRoomId) if (lastKnownEventList == notificationEvents.hashCode()) { Timber.d("Skipping notification update due to event list not changing") } else { - processEvents(notificationEvents, myUserId, myUserDisplayName, myUserAvatarUrl) + processEvents(notificationEvents, myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat) lastKnownEventList = notificationEvents.hashCode() } } - private fun processEvents(notificationEvents: ProcessedNotificationEvents, myUserId: String, myUserDisplayName: String, myUserAvatarUrl: String?) { + private fun processEvents(notificationEvents: ProcessedNotificationEvents, myUserId: String, myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean) { val (roomEvents, simpleEvents, invitationEvents) = notificationEvents with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) @@ -70,6 +72,9 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces is RoomNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) is RoomNotification.Message -> if (useCompleteNotificationFormat) { Timber.d("Updating room messages notification ${wrapper.meta.roomId}") + wrapper.shortcutInfo?.let { + ShortcutManagerCompat.pushDynamicShortcut(appContext, it) + } notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 786ce40046..56e3274515 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -16,11 +16,17 @@ package im.vector.app.features.notifications +import android.content.Context import android.graphics.Bitmap +import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.Person +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.room.detail.RoomDetailActivity import me.gujun.android.span.Span import me.gujun.android.span.span import timber.log.Timber @@ -30,7 +36,8 @@ class RoomGroupMessageCreator @Inject constructor( private val iconLoader: IconLoader, private val bitmapLoader: BitmapLoader, private val stringProvider: StringProvider, - private val notificationUtils: NotificationUtils + private val notificationUtils: NotificationUtils, + private val appContext: Context ) { fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { @@ -54,6 +61,19 @@ class RoomGroupMessageCreator @Inject constructor( stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) } + val largeBitmap = getRoomBitmap(events) + val shortcutInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val openRoomIntent = RoomDetailActivity.shortcutIntent(appContext, roomId) + ShortcutInfoCompat.Builder(appContext, roomId) + .setLongLived(true) + .setIntent(openRoomIntent) + .setShortLabel(roomName) + .setIcon(largeBitmap?.let { IconCompat.createWithAdaptiveBitmap(it) } ?: iconLoader.getUserIcon(events.last().senderAvatarPath)) + .build() + } else { + null + } + val lastMessageTimestamp = events.last().timestamp val smartReplyErrors = events.filter { it.isSmartReplyError() } val messageCount = (events.size - smartReplyErrors.size) @@ -72,22 +92,27 @@ class RoomGroupMessageCreator @Inject constructor( it.shouldBing = meta.shouldBing it.customSound = events.last().soundName }, - largeIcon = getRoomBitmap(events), + largeIcon = largeBitmap, lastMessageTimestamp, userDisplayName, tickerText ), + shortcutInfo, meta ) } private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { events.forEach { event -> - val senderPerson = Person.Builder() - .setName(event.senderName) - .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) - .setKey(event.senderId) - .build() + val senderPerson = if (event.outGoingMessage) { + null + } else { + Person.Builder() + .setName(event.senderName) + .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) + .setKey(event.senderId) + .build() + } when { event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) else -> addMessage(event.body, event.timestamp, senderPerson) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index a07dd61368..74a11b6d0d 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -45,15 +45,11 @@ class NotificationRendererTest { private val notifiableEventProcessor = FakeNotifiableEventProcessor() private val notificationDisplayer = FakeNotificationDisplayer() - private val preferences = FakeVectorPreferences().also { - it.givenUseCompleteNotificationFormat(USE_COMPLETE_NOTIFICATION_FORMAT) - } private val notificationFactory = FakeNotificationFactory() private val notificationRenderer = NotificationRenderer( notifiableEventProcessor = notifiableEventProcessor.instance, notificationDisplayer = notificationDisplayer.instance, - vectorPreferences = preferences.instance, notificationFactory = notificationFactory.instance ) @@ -154,6 +150,7 @@ class NotificationRendererTest { myUserId = MY_USER_ID, myUserDisplayName = MY_USER_DISPLAY_NAME, myUserAvatarUrl = MY_USER_AVATAR_URL, + useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, eventList = AN_EVENT_LIST ) } From 3d567d0dcdff7400f4a25ec5cb205091f420155a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 14:18:27 +0100 Subject: [PATCH 21/49] removing no longer needed hasBeenDisplayed state, the eventList is our source of truth - when events have finished being displayed they should be removed from the eventList via notification delete actions --- .../features/notifications/InviteNotifiableEvent.kt | 5 +---- .../app/features/notifications/NotifiableEvent.kt | 1 - .../features/notifications/NotifiableMessageEvent.kt | 2 -- .../notifications/NotificationDrawerManager.kt | 7 +++---- .../app/features/notifications/NotificationFactory.kt | 2 +- .../features/notifications/SimpleNotifiableEvent.kt | 5 +---- .../notifications/NotifiableEventProcessorTest.kt | 10 +++++++--- .../features/notifications/NotificationFactoryTest.kt | 2 +- 8 files changed, 14 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt index 743b3587a8..832f97bc4e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/InviteNotifiableEvent.kt @@ -29,7 +29,4 @@ data class InviteNotifiableEvent( val timestamp: Long, val soundName: String?, override val isRedacted: Boolean = false -) : NotifiableEvent { - - override var hasBeenDisplayed = false -} +) : NotifiableEvent diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt index 2f79da6795..52d8119cbb 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEvent.kt @@ -23,7 +23,6 @@ import java.io.Serializable sealed interface NotifiableEvent : Serializable { val eventId: String val editedEventId: String? - var hasBeenDisplayed: Boolean // Used to know if event should be replaced with the one coming from eventstream val canBeReplaced: Boolean diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index 4a2152c417..161c9f74a6 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -39,8 +39,6 @@ data class NotifiableMessageEvent( override val isRedacted: Boolean = false ) : NotifiableEvent { - override var hasBeenDisplayed: Boolean = false - val type: String = EventType.MESSAGE val description: String = body ?: "" val title: String = senderName ?: "" diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index a3a3e898ba..4be1f6ee6e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -101,7 +101,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // Use setOnlyAlertOnce to ensure update notification does not interfere with sound // from first notify invocation as outlined in: // https://developer.android.com/training/notify-user/build-notification#Updating - notifiableEvent.hasBeenDisplayed = false eventList.remove(existing) eventList.add(notifiableEvent) } else { @@ -144,9 +143,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context synchronized(eventList) { eventList.replace(eventId) { when (it) { - is InviteNotifiableEvent -> it.copy(isRedacted = true).apply { hasBeenDisplayed = false } - is NotifiableMessageEvent -> it.copy(isRedacted = true).apply { hasBeenDisplayed = false } - is SimpleNotifiableEvent -> it.copy(isRedacted = true).apply { hasBeenDisplayed = false } + is InviteNotifiableEvent -> it.copy(isRedacted = true) + is NotifiableMessageEvent -> it.copy(isRedacted = true) + is SimpleNotifiableEvent -> it.copy(isRedacted = true) } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index ec8e372c4e..f47e82e845 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -38,7 +38,7 @@ class NotificationFactory @Inject constructor( private fun List.hasNoEventsToDisplay() = isEmpty() || all { it.canNotBeDisplayed() } - private fun NotifiableMessageEvent.canNotBeDisplayed() = hasBeenDisplayed || isRedacted + private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted fun Map.toNotifications(myUserId: String): List { return this.map { (roomId, event) -> diff --git a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt index 940d8a3770..8c72372204 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SimpleNotifiableEvent.kt @@ -27,7 +27,4 @@ data class SimpleNotifiableEvent( val soundName: String?, override var canBeReplaced: Boolean, override val isRedacted: Boolean = false -) : NotifiableEvent { - - override var hasBeenDisplayed: Boolean = false -} +) : NotifiableEvent diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index a5bb9978dd..6f47e71500 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -156,7 +156,8 @@ fun aSimpleNotifiableEvent(eventId: String) = SimpleNotifiableEvent( type = null, timestamp = 0, soundName = null, - isPushGatewayEvent = false + canBeReplaced = false, + isRedacted = false ) fun anInviteNotifiableEvent(roomId: String) = InviteNotifiableEvent( @@ -170,7 +171,8 @@ fun anInviteNotifiableEvent(roomId: String) = InviteNotifiableEvent( type = null, timestamp = 0, soundName = null, - isPushGatewayEvent = false + canBeReplaced = false, + isRedacted = false ) fun aNotifiableMessageEvent(eventId: String, roomId: String) = NotifiableMessageEvent( @@ -183,5 +185,7 @@ fun aNotifiableMessageEvent(eventId: String, roomId: String) = NotifiableMessage body = "message-body", roomId = roomId, roomName = "room-name", - roomIsDirect = false + roomIsDirect = false, + canBeReplaced = false, + isRedacted = false ) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index c08be9e8c7..f8e6813d9b 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -123,7 +123,7 @@ class NotificationFactoryTest { @Test fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { - val redactedRoom = mapOf(A_ROOM_ID to listOf(A_MESSAGE_EVENT.copy().apply { isRedacted = true })) + val redactedRoom = mapOf(A_ROOM_ID to listOf(A_MESSAGE_EVENT.copy(isRedacted = true))) val result = redactedRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) From 0d316e69ded9a16f9da4bc05ec8b74fa82df793d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 16:03:52 +0100 Subject: [PATCH 22/49] handling creating the summary when notification events are filtered to empty due to only containing removals --- .../notifications/NotificationFactory.kt | 42 +++++++--- .../notifications/NotificationRenderer.kt | 78 ++++++++++--------- .../SummaryGroupMessageCreator.kt | 8 +- .../notifications/NotificationFactoryTest.kt | 6 +- .../notifications/NotificationRendererTest.kt | 21 ++--- .../app/test/fakes/FakeNotificationFactory.kt | 4 +- 6 files changed, 94 insertions(+), 65 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index f47e82e845..8189977ba8 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -46,7 +46,12 @@ class NotificationFactory @Inject constructor( null -> OneShotNotification.Removed(key = roomId) else -> OneShotNotification.Append( notificationUtils.buildRoomInvitationNotification(event, myUserId), - OneShotNotification.Append.Meta(key = roomId, summaryLine = event.description, isNoisy = event.noisy) + OneShotNotification.Append.Meta( + key = roomId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) ) } } @@ -59,7 +64,12 @@ class NotificationFactory @Inject constructor( null -> OneShotNotification.Removed(key = eventId) else -> OneShotNotification.Append( notificationUtils.buildSimpleEventNotification(event, myUserId), - OneShotNotification.Append.Meta(key = eventId, summaryLine = event.description, isNoisy = event.noisy) + OneShotNotification.Append.Meta( + key = eventId, + summaryLine = event.description, + isNoisy = event.noisy, + timestamp = event.timestamp + ) ) } } @@ -68,13 +78,19 @@ class NotificationFactory @Inject constructor( fun createSummaryNotification(roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - useCompleteNotificationFormat: Boolean): Notification { - return summaryGroupMessageCreator.createSummaryNotification( - roomNotifications = roomNotifications.mapToMeta(), - invitationNotifications = invitationNotifications.mapToMeta(), - simpleNotifications = simpleNotifications.mapToMeta(), - useCompleteNotificationFormat = useCompleteNotificationFormat - ) + useCompleteNotificationFormat: Boolean): SummaryNotification { + val roomMeta = roomNotifications.mapToMeta() + val invitationMeta = invitationNotifications.mapToMeta() + val simpleMeta = simpleNotifications.mapToMeta() + return when { + roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed + else -> SummaryNotification.Update(summaryGroupMessageCreator.createSummaryNotification( + roomNotifications = roomMeta, + invitationNotifications = invitationMeta, + simpleNotifications = simpleMeta, + useCompleteNotificationFormat = useCompleteNotificationFormat + )) + } } } @@ -102,7 +118,13 @@ sealed interface OneShotNotification { data class Meta( val key: String, val summaryLine: CharSequence, - val isNoisy: Boolean + val isNoisy: Boolean, + val timestamp: Long, ) } } + +sealed interface SummaryNotification { + object Removed : SummaryNotification + data class Update(val notification: Notification) : SummaryNotification +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index 005cf04f07..749e3c61b6 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -16,12 +16,8 @@ package im.vector.app.features.notifications import android.content.Context -import android.os.Build import androidx.annotation.WorkerThread -import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import im.vector.app.features.home.room.detail.RoomDetailActivity import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -41,7 +37,7 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean, eventList: MutableList) { - Timber.v("refreshNotificationDrawerBg()") + Timber.v("Render notification events - count: ${eventList.size}") val notificationEvents = notifiableEventProcessor.modifyAndProcess(eventList, currentRoomId) if (lastKnownEventList == notificationEvents.hashCode()) { Timber.d("Skipping notification update due to event list not changing") @@ -57,49 +53,55 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) val invitationNotifications = invitationEvents.toNotifications(myUserId) val simpleNotifications = simpleEvents.toNotifications(myUserId) + val summaryNotification = createSummaryNotification( + roomNotifications = roomNotifications, + invitationNotifications = invitationNotifications, + simpleNotifications = simpleNotifications, + useCompleteNotificationFormat = useCompleteNotificationFormat + ) - if (roomNotifications.isEmpty() && invitationNotifications.isEmpty() && simpleNotifications.isEmpty()) { - notificationDisplayer.cancelNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) - } else { - val summaryNotification = createSummaryNotification( - roomNotifications = roomNotifications, - invitationNotifications = invitationNotifications, - simpleNotifications = simpleNotifications, - useCompleteNotificationFormat = useCompleteNotificationFormat - ) - roomNotifications.forEach { wrapper -> - when (wrapper) { - is RoomNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) - is RoomNotification.Message -> if (useCompleteNotificationFormat) { - Timber.d("Updating room messages notification ${wrapper.meta.roomId}") - wrapper.shortcutInfo?.let { - ShortcutManagerCompat.pushDynamicShortcut(appContext, it) - } - notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) + roomNotifications.forEach { wrapper -> + when (wrapper) { + is RoomNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) + is RoomNotification.Message -> if (useCompleteNotificationFormat) { + Timber.d("Updating room messages notification ${wrapper.meta.roomId}") + wrapper.shortcutInfo?.let { + ShortcutManagerCompat.pushDynamicShortcut(appContext, it) } + notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) } } + } - invitationNotifications.forEach { wrapper -> - when (wrapper) { - is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID) - is OneShotNotification.Append -> if (useCompleteNotificationFormat) { - Timber.d("Updating invitation notification ${wrapper.meta.key}") - notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) - } + invitationNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID) + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating invitation notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) } } + } - simpleNotifications.forEach { wrapper -> - when (wrapper) { - is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID) - is OneShotNotification.Append -> if (useCompleteNotificationFormat) { - Timber.d("Updating simple notification ${wrapper.meta.key}") - notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) - } + simpleNotifications.forEach { wrapper -> + when (wrapper) { + is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID) + is OneShotNotification.Append -> if (useCompleteNotificationFormat) { + Timber.d("Updating simple notification ${wrapper.meta.key}") + notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) } } - notificationDisplayer.showNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, summaryNotification) + } + + when (summaryNotification) { + SummaryNotification.Removed -> { + Timber.d("Removing summary notification") + notificationDisplayer.cancelNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) + } + is SummaryNotification.Update -> { + Timber.d("Updating summary notification") + notificationDisplayer.showNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, summaryNotification.notification) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt index dc9ff92aa6..821f24b436 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt @@ -57,7 +57,9 @@ class SummaryGroupMessageCreator @Inject constructor( val messageCount = roomNotifications.fold(initial = 0) { acc, current -> acc + current.messageCount } - val lastMessageTimestamp1 = roomNotifications.last().latestTimestamp + val lastMessageTimestamp = roomNotifications.lastOrNull()?.latestTimestamp + ?: invitationNotifications.lastOrNull()?.timestamp + ?: simpleNotifications.last().timestamp // FIXME roomIdToEventMap.size is not correct, this is the number of rooms val nbEvents = roomNotifications.size + simpleNotifications.size @@ -71,12 +73,12 @@ class SummaryGroupMessageCreator @Inject constructor( summaryInboxStyle, sumTitle, noisy = summaryIsNoisy, - lastMessageTimestamp = lastMessageTimestamp1 + lastMessageTimestamp = lastMessageTimestamp ) } else { processSimpleGroupSummary(summaryIsNoisy, messageCount, simpleNotifications.size, invitationNotifications.size, - roomNotifications.size, lastMessageTimestamp1) + roomNotifications.size, lastMessageTimestamp) } } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index f8e6813d9b..fc20f09811 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -55,7 +55,8 @@ class NotificationFactoryTest { meta = OneShotNotification.Append.Meta( key = A_ROOM_ID, summaryLine = AN_INVITATION_EVENT.description, - isNoisy = AN_INVITATION_EVENT.noisy + isNoisy = AN_INVITATION_EVENT.noisy, + timestamp = AN_INVITATION_EVENT.timestamp )) ) } @@ -83,7 +84,8 @@ class NotificationFactoryTest { meta = OneShotNotification.Append.Meta( key = AN_EVENT_ID, summaryLine = A_SIMPLE_EVENT.description, - isNoisy = A_SIMPLE_EVENT.noisy + isNoisy = A_SIMPLE_EVENT.noisy, + timestamp = AN_INVITATION_EVENT.timestamp )) ) } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index 74a11b6d0d..e3c97ad3cf 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -34,12 +34,13 @@ private const val USE_COMPLETE_NOTIFICATION_FORMAT = true private val AN_EVENT_LIST = mutableListOf() private val A_PROCESSED_EVENTS = ProcessedNotificationEvents(emptyMap(), emptyMap(), emptyMap()) -private val A_SUMMARY_NOTIFICATION = mockk() +private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk()) +private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed private val A_NOTIFICATION = mockk() private val MESSAGE_META = RoomNotification.Message.Meta( summaryLine = "ignored", messageCount = 1, latestTimestamp = -1, roomId = A_ROOM_ID, shouldBing = false ) -private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false) +private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1) class NotificationRendererTest { @@ -71,7 +72,7 @@ class NotificationRendererTest { notificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) - showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification) } } @@ -86,7 +87,7 @@ class NotificationRendererTest { notificationDisplayer.verifyInOrder { showNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, A_NOTIFICATION) - showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification) } } @@ -98,7 +99,7 @@ class NotificationRendererTest { notificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID) - showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification) } } @@ -113,7 +114,7 @@ class NotificationRendererTest { notificationDisplayer.verifyInOrder { showNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, A_NOTIFICATION) - showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification) } } @@ -125,7 +126,7 @@ class NotificationRendererTest { notificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID) - showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification) } } @@ -140,7 +141,7 @@ class NotificationRendererTest { notificationDisplayer.verifyInOrder { showNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, A_NOTIFICATION) - showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION) + showNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, A_SUMMARY_NOTIFICATION.notification) } } @@ -156,14 +157,14 @@ class NotificationRendererTest { } private fun givenNoNotifications() { - givenNotifications(emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_SUMMARY_NOTIFICATION) + givenNotifications(emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_REMOVE_SUMMARY_NOTIFICATION) } private fun givenNotifications(roomNotifications: List = emptyList(), invitationNotifications: List = emptyList(), simpleNotifications: List = emptyList(), useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT, - summaryNotification: Notification = A_SUMMARY_NOTIFICATION) { + summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION) { notifiableEventProcessor.givenProcessedEventsFor(AN_EVENT_LIST, A_CURRENT_ROOM_ID, A_PROCESSED_EVENTS) notificationFactory.givenNotificationsFor( processedEvents = A_PROCESSED_EVENTS, diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt index 921999bd93..da2dbc27da 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt @@ -16,11 +16,11 @@ package im.vector.app.test.fakes -import android.app.Notification import im.vector.app.features.notifications.NotificationFactory import im.vector.app.features.notifications.OneShotNotification import im.vector.app.features.notifications.ProcessedNotificationEvents import im.vector.app.features.notifications.RoomNotification +import im.vector.app.features.notifications.SummaryNotification import io.mockk.every import io.mockk.mockk @@ -36,7 +36,7 @@ class FakeNotificationFactory { roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - summaryNotification: Notification) { + summaryNotification: SummaryNotification) { with(instance) { every { processedEvents.roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) } returns roomNotifications every { processedEvents.invitationEvents.toNotifications(myUserId) } returns invitationNotifications From 8fb6bef503a97720805df19029f4e7c104017ee7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 16:29:20 +0100 Subject: [PATCH 23/49] removing this usages for project convention --- .../app/features/notifications/NotificationFactory.kt | 6 +++--- .../app/features/notifications/RoomGroupMessageCreator.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 8189977ba8..0c3aab7bd3 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -28,7 +28,7 @@ class NotificationFactory @Inject constructor( ) { fun Map>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { - return this.map { (roomId, events) -> + return map { (roomId, events) -> when { events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) else -> roomGroupMessageCreator.createRoomMessage(events, roomId, myUserDisplayName, myUserAvatarUrl) @@ -41,7 +41,7 @@ class NotificationFactory @Inject constructor( private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted fun Map.toNotifications(myUserId: String): List { - return this.map { (roomId, event) -> + return map { (roomId, event) -> when (event) { null -> OneShotNotification.Removed(key = roomId) else -> OneShotNotification.Append( @@ -59,7 +59,7 @@ class NotificationFactory @Inject constructor( @JvmName("toNotificationsSimpleNotifiableEvent") fun Map.toNotifications(myUserId: String): List { - return this.map { (eventId, event) -> + return map { (eventId, event) -> when (event) { null -> OneShotNotification.Removed(key = eventId) else -> OneShotNotification.Append( diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 56e3274515..113dc21ebd 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -170,4 +170,4 @@ class RoomGroupMessageCreator @Inject constructor( } } -private fun NotifiableMessageEvent.isSmartReplyError() = this.outGoingMessage && this.outGoingMessageFailed +private fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed From a94a1a0523340dc690ed8413509d94a95b7be757 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 7 Oct 2021 16:32:00 +0100 Subject: [PATCH 24/49] formatting --- .../notifications/NotificationFactory.kt | 13 +++++----- .../notifications/NotificationRenderer.kt | 26 ++++++++++++------- .../SummaryGroupMessageCreator.kt | 6 ++--- .../notifications/NotificationRendererTest.kt | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 0c3aab7bd3..d5de0221f6 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -84,12 +84,13 @@ class NotificationFactory @Inject constructor( val simpleMeta = simpleNotifications.mapToMeta() return when { roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed - else -> SummaryNotification.Update(summaryGroupMessageCreator.createSummaryNotification( - roomNotifications = roomMeta, - invitationNotifications = invitationMeta, - simpleNotifications = simpleMeta, - useCompleteNotificationFormat = useCompleteNotificationFormat - )) + else -> SummaryNotification.Update( + summaryGroupMessageCreator.createSummaryNotification( + roomNotifications = roomMeta, + invitationNotifications = invitationMeta, + simpleNotifications = simpleMeta, + useCompleteNotificationFormat = useCompleteNotificationFormat + )) } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index 749e3c61b6..844ea46f93 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -17,6 +17,10 @@ package im.vector.app.features.notifications import android.content.Context import androidx.annotation.WorkerThread +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.SUMMARY_NOTIFICATION_ID import androidx.core.content.pm.ShortcutManagerCompat import timber.log.Timber import javax.inject.Inject @@ -47,7 +51,11 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces } } - private fun processEvents(notificationEvents: ProcessedNotificationEvents, myUserId: String, myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean) { + private fun processEvents(notificationEvents: ProcessedNotificationEvents, + myUserId: String, + myUserDisplayName: String, + myUserAvatarUrl: String?, + useCompleteNotificationFormat: Boolean) { val (roomEvents, simpleEvents, invitationEvents) = notificationEvents with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) @@ -62,33 +70,33 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces roomNotifications.forEach { wrapper -> when (wrapper) { - is RoomNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) + is RoomNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.roomId, ROOM_MESSAGES_NOTIFICATION_ID) is RoomNotification.Message -> if (useCompleteNotificationFormat) { Timber.d("Updating room messages notification ${wrapper.meta.roomId}") wrapper.shortcutInfo?.let { ShortcutManagerCompat.pushDynamicShortcut(appContext, it) } - notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) + notificationDisplayer.showNotificationMessage(wrapper.meta.roomId, ROOM_MESSAGES_NOTIFICATION_ID, wrapper.notification) } } } invitationNotifications.forEach { wrapper -> when (wrapper) { - is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID) + is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_INVITATION_NOTIFICATION_ID) is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating invitation notification ${wrapper.meta.key}") - notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) + notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) } } } simpleNotifications.forEach { wrapper -> when (wrapper) { - is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID) + is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_EVENT_NOTIFICATION_ID) is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating simple notification ${wrapper.meta.key}") - notificationDisplayer.showNotificationMessage(wrapper.meta.key, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) + notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) } } } @@ -96,11 +104,11 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces when (summaryNotification) { SummaryNotification.Removed -> { Timber.d("Removing summary notification") - notificationDisplayer.cancelNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) + notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) } is SummaryNotification.Update -> { Timber.d("Updating summary notification") - notificationDisplayer.showNotificationMessage(null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID, summaryNotification.notification) + notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification) } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt index 821f24b436..ddef31a0f5 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt @@ -51,9 +51,9 @@ class SummaryGroupMessageCreator @Inject constructor( simpleNotifications.forEach { style.addLine(it.summaryLine) } } - val summaryIsNoisy = roomNotifications.any { it.shouldBing } - || invitationNotifications.any { it.isNoisy } - || simpleNotifications.any { it.isNoisy } + val summaryIsNoisy = roomNotifications.any { it.shouldBing } || + invitationNotifications.any { it.isNoisy } || + simpleNotifications.any { it.isNoisy } val messageCount = roomNotifications.fold(initial = 0) { acc, current -> acc + current.messageCount } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index e3c97ad3cf..b0f1772fc7 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -20,7 +20,6 @@ import android.app.Notification import im.vector.app.test.fakes.FakeNotifiableEventProcessor import im.vector.app.test.fakes.FakeNotificationDisplayer import im.vector.app.test.fakes.FakeNotificationFactory -import im.vector.app.test.fakes.FakeVectorPreferences import io.mockk.mockk import org.junit.Test From 03fe45da609e61437ace8c71e109c5268733e2e4 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 8 Oct 2021 14:50:53 +0100 Subject: [PATCH 25/49] ensuring that we removing the summary group before removing individual notifications - adds some comments to explain the positioning --- .../notifications/NotificationRenderer.kt | 28 +++++++++++---- .../notifications/NotificationRendererTest.kt | 36 +++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index 844ea46f93..73ea65debc 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -68,9 +68,20 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces useCompleteNotificationFormat = useCompleteNotificationFormat ) + // Remove summary first to avoid briefly displaying it after dismissing the last notification + when (summaryNotification) { + SummaryNotification.Removed -> { + Timber.d("Removing summary notification") + notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) + } + } + roomNotifications.forEach { wrapper -> when (wrapper) { - is RoomNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.roomId, ROOM_MESSAGES_NOTIFICATION_ID) + is RoomNotification.Removed -> { + Timber.d("Removing room messages notification ${wrapper.roomId}") + notificationDisplayer.cancelNotificationMessage(wrapper.roomId, ROOM_MESSAGES_NOTIFICATION_ID) + } is RoomNotification.Message -> if (useCompleteNotificationFormat) { Timber.d("Updating room messages notification ${wrapper.meta.roomId}") wrapper.shortcutInfo?.let { @@ -83,7 +94,10 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces invitationNotifications.forEach { wrapper -> when (wrapper) { - is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_INVITATION_NOTIFICATION_ID) + is OneShotNotification.Removed -> { + Timber.d("Removing invitation notification ${wrapper.key}") + notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_INVITATION_NOTIFICATION_ID) + } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating invitation notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_INVITATION_NOTIFICATION_ID, wrapper.notification) @@ -93,7 +107,10 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces simpleNotifications.forEach { wrapper -> when (wrapper) { - is OneShotNotification.Removed -> notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_EVENT_NOTIFICATION_ID) + is OneShotNotification.Removed -> { + Timber.d("Removing simple notification ${wrapper.key}") + notificationDisplayer.cancelNotificationMessage(wrapper.key, ROOM_EVENT_NOTIFICATION_ID) + } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating simple notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage(wrapper.meta.key, ROOM_EVENT_NOTIFICATION_ID, wrapper.notification) @@ -101,11 +118,8 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces } } + // Update summary last to avoid briefly displaying it before other notifications when (summaryNotification) { - SummaryNotification.Removed -> { - Timber.d("Removing summary notification") - notificationDisplayer.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) - } is SummaryNotification.Update -> { Timber.d("Updating summary notification") notificationDisplayer.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, summaryNotification.notification) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index b0f1772fc7..1c68dc4f68 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -63,6 +63,18 @@ class NotificationRendererTest { notificationDisplayer.verifyNoOtherInteractions() } + @Test + fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() { + givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) + cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_MESSAGES_NOTIFICATION_ID) + } + } + @Test fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() { givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID))) @@ -90,6 +102,18 @@ class NotificationRendererTest { } } + @Test + fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() { + givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) + cancelNotificationMessage(tag = AN_EVENT_ID, NotificationDrawerManager.ROOM_EVENT_NOTIFICATION_ID) + } + } + @Test fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() { givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID))) @@ -117,6 +141,18 @@ class NotificationRendererTest { } } + @Test + fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() { + givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) + + renderEventsAsNotifications() + + notificationDisplayer.verifyInOrder { + cancelNotificationMessage(tag = null, NotificationDrawerManager.SUMMARY_NOTIFICATION_ID) + cancelNotificationMessage(tag = A_ROOM_ID, NotificationDrawerManager.ROOM_INVITATION_NOTIFICATION_ID) + } + } + @Test fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() { givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID))) From 587466e0096c2dc3193dabd39d748d2cdf80e2f3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Oct 2021 09:55:26 +0100 Subject: [PATCH 26/49] relying on the notification refreshing to cancel/update the notifications --- .../features/notifications/NotificationDrawerManager.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 4be1f6ee6e..042d2f1f88 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -165,14 +165,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context fun clearMessageEventOfRoom(roomId: String?) { Timber.v("clearMessageEventOfRoom $roomId") if (roomId != null) { - var shouldUpdate = false - synchronized(eventList) { - shouldUpdate = eventList.removeAll { e -> - e is NotifiableMessageEvent && e.roomId == roomId - } - } + val shouldUpdate = removeAll { it is NotifiableMessageEvent && it.roomId == roomId } if (shouldUpdate) { - notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) refreshNotificationDrawer() } } From b7b4c01bde5a33e669f7b97356ab3c55719a2a43 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Oct 2021 14:45:41 +0100 Subject: [PATCH 27/49] splitting the event processing from the rendering - this allows us to only synchronise of the event list modifications rather than the entire notification creation/rendering which should in turn reduce some of our ANRs https://github.com/vector-im/element-android/issues/4214 --- .../notifications/NotifiableEventProcessor.kt | 48 ++++------------- .../NotificationDrawerManager.kt | 36 ++++++++----- .../notifications/NotificationRenderer.kt | 54 ++++++++++--------- .../NotifiableEventProcessorTest.kt | 14 ++--- .../notifications/NotificationRendererTest.kt | 14 ++--- .../fakes/FakeNotifiableEventProcessor.kt | 6 --- .../app/test/fakes/FakeNotificationFactory.kt | 10 ++-- 7 files changed, 77 insertions(+), 105 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index 3f77ce54ca..bf9e805fc8 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -17,7 +17,6 @@ package im.vector.app.features.notifications import im.vector.app.features.invite.AutoAcceptInvites -import timber.log.Timber import javax.inject.Inject class NotifiableEventProcessor @Inject constructor( @@ -25,49 +24,20 @@ class NotifiableEventProcessor @Inject constructor( private val autoAcceptInvites: AutoAcceptInvites ) { - fun modifyAndProcess(eventList: MutableList, currentRoomId: String?): ProcessedNotificationEvents { - val roomIdToEventMap: MutableMap> = LinkedHashMap() - val simpleEvents: MutableMap = LinkedHashMap() - val invitationEvents: MutableMap = LinkedHashMap() - - val eventIterator = eventList.listIterator() - while (eventIterator.hasNext()) { - when (val event = eventIterator.next()) { - is NotifiableMessageEvent -> { - val roomId = event.roomId - val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } - - // should we limit to last 7 messages per room? - if (shouldIgnoreMessageEventInRoom(currentRoomId, roomId) || outdatedDetector.isMessageOutdated(event)) { - // forget this event - eventIterator.remove() - } else { - roomEvents.add(event) + fun process(eventList: List, currentRoomId: String?): Map { + return eventList.associateBy { it.eventId } + .mapValues { (_, value) -> + when (value) { + is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) null else value + is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, value.roomId) || outdatedDetector.isMessageOutdated(value)) { + null + } else value + is SimpleNotifiableEvent -> value } } - is InviteNotifiableEvent -> { - if (autoAcceptInvites.hideInvites) { - // Forget this event - eventIterator.remove() - invitationEvents[event.roomId] = null - } else { - invitationEvents[event.roomId] = event - } - } - is SimpleNotifiableEvent -> simpleEvents[event.eventId] = event - else -> Timber.w("Type not handled") - } - } - return ProcessedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) } private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean { return currentRoomId != null && roomId == currentRoomId } } - -data class ProcessedNotificationEvents( - val roomEvents: Map>, - val simpleEvents: Map, - val invitationEvents: Map -) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 042d2f1f88..43d9eff185 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -44,6 +44,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private val notificationUtils: NotificationUtils, private val vectorPreferences: VectorPreferences, private val activeSessionDataSource: ActiveSessionDataSource, + private val notifiableEventProcessor: NotifiableEventProcessor, private val notificationRenderer: NotificationRenderer) { private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY) @@ -55,6 +56,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } private val eventList = loadEventInfo() + private var renderedEventsList = emptyMap() private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentRoomId: String? = null @@ -223,22 +225,32 @@ class NotificationDrawerManager @Inject constructor(private val context: Context @WorkerThread private fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") - val session = currentSession ?: return - val user = session.getUser(session.myUserId) - // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash - val myUserDisplayName = user?.toMatrixItem()?.getBestName() ?: session.myUserId - val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE) + val newSettings = vectorPreferences.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationUtils.cancelAllNotifications() + useCompleteNotificationFormat = newSettings + } - synchronized(eventList) { - val newSettings = vectorPreferences.useCompleteNotificationFormat() - if (newSettings != useCompleteNotificationFormat) { - // Settings has changed, remove all current notifications - notificationUtils.cancelAllNotifications() - useCompleteNotificationFormat = newSettings + val eventsToRender = synchronized(eventList) { + notifiableEventProcessor.process(eventList, currentRoomId).also { + eventList.clear() + eventList.addAll(it.values.filterNotNull()) } + } - notificationRenderer.render(currentRoomId, session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventList) + if (renderedEventsList == eventsToRender) { + Timber.d("Skipping notification update due to event list not changing") + } else { + renderedEventsList = eventsToRender + val session = currentSession ?: return + val user = session.getUser(session.myUserId) + // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash + val myUserDisplayName = user?.toMatrixItem()?.getBestName() ?: session.myUserId + val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE) + + notificationRenderer.render(session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventsToRender) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index 73ea65debc..80391b1e06 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -15,7 +15,6 @@ */ package im.vector.app.features.notifications -import android.content.Context import androidx.annotation.WorkerThread import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID @@ -27,36 +26,16 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class NotificationRenderer @Inject constructor(private val notifiableEventProcessor: NotifiableEventProcessor, - private val notificationDisplayer: NotificationDisplayer, - private val notificationFactory: NotificationFactory, - private val appContext: Context) { - - private var lastKnownEventList = -1 +class NotificationRenderer @Inject constructor(private val notificationDisplayer: NotificationDisplayer, + private val notificationFactory: NotificationFactory) { @WorkerThread - fun render(currentRoomId: String?, - myUserId: String, + fun render(myUserId: String, myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean, - eventList: MutableList) { - Timber.v("Render notification events - count: ${eventList.size}") - val notificationEvents = notifiableEventProcessor.modifyAndProcess(eventList, currentRoomId) - if (lastKnownEventList == notificationEvents.hashCode()) { - Timber.d("Skipping notification update due to event list not changing") - } else { - processEvents(notificationEvents, myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat) - lastKnownEventList = notificationEvents.hashCode() - } - } - - private fun processEvents(notificationEvents: ProcessedNotificationEvents, - myUserId: String, - myUserDisplayName: String, - myUserAvatarUrl: String?, - useCompleteNotificationFormat: Boolean) { - val (roomEvents, simpleEvents, invitationEvents) = notificationEvents + eventsToProcess: Map) { + val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) val invitationNotifications = invitationEvents.toNotifications(myUserId) @@ -128,3 +107,26 @@ class NotificationRenderer @Inject constructor(private val notifiableEventProces } } } + +private fun Map.groupByType(): GroupedNotificationEvents { + val roomIdToEventMap: MutableMap> = LinkedHashMap() + val simpleEvents: MutableMap = LinkedHashMap() + val invitationEvents: MutableMap = LinkedHashMap() + forEach { (_, value) -> + when (value) { + is InviteNotifiableEvent -> invitationEvents[value.roomId] + is NotifiableMessageEvent -> { + val roomEvents = roomIdToEventMap.getOrPut(value.roomId) { ArrayList() } + roomEvents.add(value) + } + is SimpleNotifiableEvent -> simpleEvents[value.eventId] = value + } + } + return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) +} + +data class GroupedNotificationEvents( + val roomEvents: Map>, + val simpleEvents: Map, + val invitationEvents: Map +) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index 6f47e71500..3e66f82bc3 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -37,7 +37,7 @@ class NotifiableEventProcessorTest { aSimpleNotifiableEvent(eventId = "event-2") ) - val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) result shouldBeEqualTo aProcessedNotificationEvents( simpleEvents = mapOf( @@ -56,7 +56,7 @@ class NotifiableEventProcessorTest { anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) result shouldBeEqualTo aProcessedNotificationEvents( invitationEvents = mapOf( @@ -75,7 +75,7 @@ class NotifiableEventProcessorTest { anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) result shouldBeEqualTo aProcessedNotificationEvents( invitationEvents = mapOf( @@ -91,7 +91,7 @@ class NotifiableEventProcessorTest { val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsOutOfDate(events[0]) - val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) result shouldBeEqualTo aProcessedNotificationEvents( roomEvents = mapOf( @@ -106,7 +106,7 @@ class NotifiableEventProcessorTest { val (events, originalEvents) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsInDate(events[0]) - val result = eventProcessor.modifyAndProcess(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) result shouldBeEqualTo aProcessedNotificationEvents( roomEvents = mapOf( @@ -120,7 +120,7 @@ class NotifiableEventProcessorTest { fun `given viewing the same room as message event when processing then removes message`() { val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) - val result = eventProcessor.modifyAndProcess(events, currentRoomId = "room-1") + val result = eventProcessor.process(events, currentRoomId = "room-1") result shouldBeEqualTo aProcessedNotificationEvents( roomEvents = mapOf( @@ -140,7 +140,7 @@ fun createEventsList(vararg event: NotifiableEvent): Pair = emptyMap(), invitationEvents: Map = emptyMap(), roomEvents: Map> = emptyMap() -) = ProcessedNotificationEvents( +) = GroupedNotificationEvents( roomEvents = roomEvents, simpleEvents = simpleEvents, invitationEvents = invitationEvents, diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index 1c68dc4f68..bd0d1e8d3f 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -17,13 +17,11 @@ package im.vector.app.features.notifications import android.app.Notification -import im.vector.app.test.fakes.FakeNotifiableEventProcessor import im.vector.app.test.fakes.FakeNotificationDisplayer import im.vector.app.test.fakes.FakeNotificationFactory import io.mockk.mockk import org.junit.Test -private const val A_CURRENT_ROOM_ID = "current-room-id" private const val MY_USER_ID = "my-user-id" private const val MY_USER_DISPLAY_NAME = "display-name" private const val MY_USER_AVATAR_URL = "avatar-url" @@ -31,8 +29,8 @@ private const val AN_EVENT_ID = "event-id" private const val A_ROOM_ID = "room-id" private const val USE_COMPLETE_NOTIFICATION_FORMAT = true -private val AN_EVENT_LIST = mutableListOf() -private val A_PROCESSED_EVENTS = ProcessedNotificationEvents(emptyMap(), emptyMap(), emptyMap()) +private val AN_EVENT_LIST = mapOf() +private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyMap(), emptyMap()) private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk()) private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed private val A_NOTIFICATION = mockk() @@ -43,12 +41,10 @@ private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", sum class NotificationRendererTest { - private val notifiableEventProcessor = FakeNotifiableEventProcessor() private val notificationDisplayer = FakeNotificationDisplayer() private val notificationFactory = FakeNotificationFactory() private val notificationRenderer = NotificationRenderer( - notifiableEventProcessor = notifiableEventProcessor.instance, notificationDisplayer = notificationDisplayer.instance, notificationFactory = notificationFactory.instance ) @@ -182,12 +178,11 @@ class NotificationRendererTest { private fun renderEventsAsNotifications() { notificationRenderer.render( - currentRoomId = A_CURRENT_ROOM_ID, myUserId = MY_USER_ID, myUserDisplayName = MY_USER_DISPLAY_NAME, myUserAvatarUrl = MY_USER_AVATAR_URL, useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, - eventList = AN_EVENT_LIST + eventsToProcess = AN_EVENT_LIST ) } @@ -200,9 +195,8 @@ class NotificationRendererTest { simpleNotifications: List = emptyList(), useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT, summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION) { - notifiableEventProcessor.givenProcessedEventsFor(AN_EVENT_LIST, A_CURRENT_ROOM_ID, A_PROCESSED_EVENTS) notificationFactory.givenNotificationsFor( - processedEvents = A_PROCESSED_EVENTS, + groupedEvents = A_PROCESSED_EVENTS, myUserId = MY_USER_ID, myUserDisplayName = MY_USER_DISPLAY_NAME, myUserAvatarUrl = MY_USER_AVATAR_URL, diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt index 93f5e40524..6143c7a907 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt @@ -16,17 +16,11 @@ package im.vector.app.test.fakes -import im.vector.app.features.notifications.NotifiableEvent import im.vector.app.features.notifications.NotifiableEventProcessor -import im.vector.app.features.notifications.ProcessedNotificationEvents -import io.mockk.every import io.mockk.mockk class FakeNotifiableEventProcessor { val instance = mockk() - fun givenProcessedEventsFor(events: MutableList, currentRoomId: String?, processedEvents: ProcessedNotificationEvents) { - every { instance.modifyAndProcess(events, currentRoomId) } returns processedEvents - } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt index da2dbc27da..cc6f84f813 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt @@ -18,7 +18,7 @@ package im.vector.app.test.fakes import im.vector.app.features.notifications.NotificationFactory import im.vector.app.features.notifications.OneShotNotification -import im.vector.app.features.notifications.ProcessedNotificationEvents +import im.vector.app.features.notifications.GroupedNotificationEvents import im.vector.app.features.notifications.RoomNotification import im.vector.app.features.notifications.SummaryNotification import io.mockk.every @@ -28,7 +28,7 @@ class FakeNotificationFactory { val instance = mockk() - fun givenNotificationsFor(processedEvents: ProcessedNotificationEvents, + fun givenNotificationsFor(groupedEvents: GroupedNotificationEvents, myUserId: String, myUserDisplayName: String, myUserAvatarUrl: String?, @@ -38,9 +38,9 @@ class FakeNotificationFactory { simpleNotifications: List, summaryNotification: SummaryNotification) { with(instance) { - every { processedEvents.roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) } returns roomNotifications - every { processedEvents.invitationEvents.toNotifications(myUserId) } returns invitationNotifications - every { processedEvents.simpleEvents.toNotifications(myUserId) } returns simpleNotifications + every { groupedEvents.roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) } returns roomNotifications + every { groupedEvents.invitationEvents.toNotifications(myUserId) } returns invitationNotifications + every { groupedEvents.simpleEvents.toNotifications(myUserId) } returns simpleNotifications every { createSummaryNotification( From b27fb264fcff925419c18b9d8b9e36992e5bc77a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Oct 2021 15:32:12 +0100 Subject: [PATCH 28/49] using a process state of keep/removed rather than mapping to an ignored event id - this state will be used to diff the currently rendered events against the new ones --- .../notifications/NotifiableEventProcessor.kt | 30 ++++++++++++------- .../NotificationDrawerManager.kt | 6 ++-- .../notifications/NotificationFactory.kt | 24 +++++++-------- .../notifications/NotificationRenderer.kt | 27 +++++++++-------- .../NotifiableEventProcessorTest.kt | 12 ++++---- 5 files changed, 56 insertions(+), 43 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index bf9e805fc8..782f70645b 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -17,6 +17,8 @@ package im.vector.app.features.notifications import im.vector.app.features.invite.AutoAcceptInvites +import im.vector.app.features.notifications.Processed.KEEP +import im.vector.app.features.notifications.Processed.REMOVE import javax.inject.Inject class NotifiableEventProcessor @Inject constructor( @@ -24,20 +26,26 @@ class NotifiableEventProcessor @Inject constructor( private val autoAcceptInvites: AutoAcceptInvites ) { - fun process(eventList: List, currentRoomId: String?): Map { - return eventList.associateBy { it.eventId } - .mapValues { (_, value) -> - when (value) { - is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) null else value - is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, value.roomId) || outdatedDetector.isMessageOutdated(value)) { - null - } else value - is SimpleNotifiableEvent -> value - } - } + fun process(eventList: List, currentRoomId: String?): List> { + return eventList.map { + when (it) { + is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP + is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) || outdatedDetector.isMessageOutdated(it)) { + REMOVE + } else KEEP + is SimpleNotifiableEvent -> KEEP + } to it + } } private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean { return currentRoomId != null && roomId == currentRoomId } } + +enum class Processed { + KEEP, + REMOVE +} + +fun List>.onlyKeptEvents() = filter { it.first == KEEP }.map { it.second } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 43d9eff185..c73b4b2a9c 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -56,7 +56,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } private val eventList = loadEventInfo() - private var renderedEventsList = emptyMap() + private var renderedEventsList = emptyList>() private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentRoomId: String? = null @@ -236,10 +236,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val eventsToRender = synchronized(eventList) { notifiableEventProcessor.process(eventList, currentRoomId).also { eventList.clear() - eventList.addAll(it.values.filterNotNull()) + eventList.addAll(it.onlyKeptEvents()) } } + + if (renderedEventsList == eventsToRender) { Timber.d("Skipping notification update due to event list not changing") } else { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index d5de0221f6..88f21a02a6 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -40,14 +40,14 @@ class NotificationFactory @Inject constructor( private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted - fun Map.toNotifications(myUserId: String): List { - return map { (roomId, event) -> - when (event) { - null -> OneShotNotification.Removed(key = roomId) - else -> OneShotNotification.Append( + fun List>.toNotifications(myUserId: String): List { + return map { (processed, event) -> + when (processed) { + Processed.REMOVE -> OneShotNotification.Removed(key = event.roomId) + Processed.KEEP -> OneShotNotification.Append( notificationUtils.buildRoomInvitationNotification(event, myUserId), OneShotNotification.Append.Meta( - key = roomId, + key = event.roomId, summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp @@ -58,14 +58,14 @@ class NotificationFactory @Inject constructor( } @JvmName("toNotificationsSimpleNotifiableEvent") - fun Map.toNotifications(myUserId: String): List { - return map { (eventId, event) -> - when (event) { - null -> OneShotNotification.Removed(key = eventId) - else -> OneShotNotification.Append( + fun List>.toNotifications(myUserId: String): List { + return map { (processed, event) -> + when (processed) { + Processed.REMOVE -> OneShotNotification.Removed(key = event.eventId) + Processed.KEEP -> OneShotNotification.Append( notificationUtils.buildSimpleEventNotification(event, myUserId), OneShotNotification.Append.Meta( - key = eventId, + key = event.eventId, summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index 80391b1e06..31a99810e0 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -34,7 +34,7 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean, - eventsToProcess: Map) { + eventsToProcess: List>) { val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) @@ -108,25 +108,28 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer } } -private fun Map.groupByType(): GroupedNotificationEvents { +private fun List>.groupByType(): GroupedNotificationEvents { val roomIdToEventMap: MutableMap> = LinkedHashMap() - val simpleEvents: MutableMap = LinkedHashMap() - val invitationEvents: MutableMap = LinkedHashMap() - forEach { (_, value) -> - when (value) { - is InviteNotifiableEvent -> invitationEvents[value.roomId] + val simpleEvents: MutableList> = ArrayList() + val invitationEvents: MutableList> = ArrayList() + forEach { + when (val event = it.second) { + is InviteNotifiableEvent -> invitationEvents.add(it.asPair()) is NotifiableMessageEvent -> { - val roomEvents = roomIdToEventMap.getOrPut(value.roomId) { ArrayList() } - roomEvents.add(value) + val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() } + roomEvents.add(event) } - is SimpleNotifiableEvent -> simpleEvents[value.eventId] = value + is SimpleNotifiableEvent -> simpleEvents.add(it.asPair()) } } return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) } +@Suppress("UNCHECKED_CAST") +private fun Pair.asPair(): Pair = this as Pair + data class GroupedNotificationEvents( val roomEvents: Map>, - val simpleEvents: Map, - val invitationEvents: Map + val simpleEvents: List>, + val invitationEvents: List> ) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index 3e66f82bc3..154763afae 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -37,7 +37,7 @@ class NotifiableEventProcessorTest { aSimpleNotifiableEvent(eventId = "event-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) result shouldBeEqualTo aProcessedNotificationEvents( simpleEvents = mapOf( @@ -56,7 +56,7 @@ class NotifiableEventProcessorTest { anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) result shouldBeEqualTo aProcessedNotificationEvents( invitationEvents = mapOf( @@ -75,7 +75,7 @@ class NotifiableEventProcessorTest { anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) result shouldBeEqualTo aProcessedNotificationEvents( invitationEvents = mapOf( @@ -91,7 +91,7 @@ class NotifiableEventProcessorTest { val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsOutOfDate(events[0]) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) result shouldBeEqualTo aProcessedNotificationEvents( roomEvents = mapOf( @@ -106,7 +106,7 @@ class NotifiableEventProcessorTest { val (events, originalEvents) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsInDate(events[0]) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) result shouldBeEqualTo aProcessedNotificationEvents( roomEvents = mapOf( @@ -120,7 +120,7 @@ class NotifiableEventProcessorTest { fun `given viewing the same room as message event when processing then removes message`() { val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) - val result = eventProcessor.process(events, currentRoomId = "room-1") + val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEventsList = renderedEventsList) result shouldBeEqualTo aProcessedNotificationEvents( roomEvents = mapOf( From 0bdc65b47f65a395f1c602aed5b95a4e42bdf39b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Oct 2021 16:39:52 +0100 Subject: [PATCH 29/49] diffing the notification events against the currently rendered events allow us to dismiss notifications from removed events --- .../notifications/NotifiableEventProcessor.kt | 21 ++-- .../NotificationDrawerManager.kt | 6 +- .../notifications/NotificationFactory.kt | 12 +- .../notifications/NotificationRenderer.kt | 14 +-- .../features/notifications/ProcessedEvent.kt} | 14 +-- .../NotifiableEventProcessorTest.kt | 111 ++++++++---------- .../notifications/NotificationFactoryTest.kt | 8 +- .../notifications/NotificationRendererTest.kt | 4 +- .../app/test/fakes/FakeNotificationFactory.kt | 3 +- 9 files changed, 86 insertions(+), 107 deletions(-) rename vector/src/{test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt => main/java/im/vector/app/features/notifications/ProcessedEvent.kt} (69%) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index 782f70645b..88dc455e20 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -17,8 +17,8 @@ package im.vector.app.features.notifications import im.vector.app.features.invite.AutoAcceptInvites -import im.vector.app.features.notifications.Processed.KEEP -import im.vector.app.features.notifications.Processed.REMOVE +import im.vector.app.features.notifications.ProcessedType.KEEP +import im.vector.app.features.notifications.ProcessedType.REMOVE import javax.inject.Inject class NotifiableEventProcessor @Inject constructor( @@ -26,8 +26,8 @@ class NotifiableEventProcessor @Inject constructor( private val autoAcceptInvites: AutoAcceptInvites ) { - fun process(eventList: List, currentRoomId: String?): List> { - return eventList.map { + fun process(eventList: List, currentRoomId: String?, renderedEventsList: List>): List { + val processedEventList = eventList.map { when (it) { is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) || outdatedDetector.isMessageOutdated(it)) { @@ -36,16 +36,15 @@ class NotifiableEventProcessor @Inject constructor( is SimpleNotifiableEvent -> KEEP } to it } + + val removedEventsDiff = renderedEventsList.filter { renderedEvent -> + eventList.none { it.eventId == renderedEvent.second.eventId } + }.map { REMOVE to it.second } + + return removedEventsDiff + processedEventList } private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean { return currentRoomId != null && roomId == currentRoomId } } - -enum class Processed { - KEEP, - REMOVE -} - -fun List>.onlyKeptEvents() = filter { it.first == KEEP }.map { it.second } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index c73b4b2a9c..b7bb20237c 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -56,7 +56,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } private val eventList = loadEventInfo() - private var renderedEventsList = emptyList>() + private var renderedEventsList = emptyList>() private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentRoomId: String? = null @@ -234,14 +234,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } val eventsToRender = synchronized(eventList) { - notifiableEventProcessor.process(eventList, currentRoomId).also { + notifiableEventProcessor.process(eventList, currentRoomId, renderedEventsList).also { eventList.clear() eventList.addAll(it.onlyKeptEvents()) } } - - if (renderedEventsList == eventsToRender) { Timber.d("Skipping notification update due to event list not changing") } else { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 88f21a02a6..fe1671b58b 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -40,11 +40,11 @@ class NotificationFactory @Inject constructor( private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted - fun List>.toNotifications(myUserId: String): List { + fun List>.toNotifications(myUserId: String): List { return map { (processed, event) -> when (processed) { - Processed.REMOVE -> OneShotNotification.Removed(key = event.roomId) - Processed.KEEP -> OneShotNotification.Append( + ProcessedType.REMOVE -> OneShotNotification.Removed(key = event.roomId) + ProcessedType.KEEP -> OneShotNotification.Append( notificationUtils.buildRoomInvitationNotification(event, myUserId), OneShotNotification.Append.Meta( key = event.roomId, @@ -58,11 +58,11 @@ class NotificationFactory @Inject constructor( } @JvmName("toNotificationsSimpleNotifiableEvent") - fun List>.toNotifications(myUserId: String): List { + fun List>.toNotifications(myUserId: String): List { return map { (processed, event) -> when (processed) { - Processed.REMOVE -> OneShotNotification.Removed(key = event.eventId) - Processed.KEEP -> OneShotNotification.Append( + ProcessedType.REMOVE -> OneShotNotification.Removed(key = event.eventId) + ProcessedType.KEEP -> OneShotNotification.Append( notificationUtils.buildSimpleEventNotification(event, myUserId), OneShotNotification.Append.Meta( key = event.eventId, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index 31a99810e0..7cf0a8872a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -34,7 +34,7 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean, - eventsToProcess: List>) { + eventsToProcess: List>) { val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) @@ -108,10 +108,10 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer } } -private fun List>.groupByType(): GroupedNotificationEvents { +private fun List>.groupByType(): GroupedNotificationEvents { val roomIdToEventMap: MutableMap> = LinkedHashMap() - val simpleEvents: MutableList> = ArrayList() - val invitationEvents: MutableList> = ArrayList() + val simpleEvents: MutableList> = ArrayList() + val invitationEvents: MutableList> = ArrayList() forEach { when (val event = it.second) { is InviteNotifiableEvent -> invitationEvents.add(it.asPair()) @@ -126,10 +126,10 @@ private fun List>.groupByType(): GroupedNotific } @Suppress("UNCHECKED_CAST") -private fun Pair.asPair(): Pair = this as Pair +private fun Pair.asPair(): Pair = this as Pair data class GroupedNotificationEvents( val roomEvents: Map>, - val simpleEvents: List>, - val invitationEvents: List> + val simpleEvents: List>, + val invitationEvents: List> ) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt similarity index 69% rename from vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt rename to vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt index 6143c7a907..0901757d02 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeNotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package im.vector.app.test.fakes +package im.vector.app.features.notifications -import im.vector.app.features.notifications.NotifiableEventProcessor -import io.mockk.mockk - -class FakeNotifiableEventProcessor { - - val instance = mockk() +typealias ProcessedEvent = Pair +enum class ProcessedType { + KEEP, + REMOVE } + +fun List.onlyKeptEvents() = filter { it.first == ProcessedType.KEEP }.map { it.second } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index 154763afae..1c0f5f9390 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -31,121 +31,104 @@ class NotifiableEventProcessorTest { private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance, autoAcceptInvites) @Test - fun `given simple events when processing then return without mutating`() { - val (events, originalEvents) = createEventsList( + fun `given simple events when processing then keep simple events`() { + val events = listOf( aSimpleNotifiableEvent(eventId = "event-1"), aSimpleNotifiableEvent(eventId = "event-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo aProcessedNotificationEvents( - simpleEvents = mapOf( - "event-1" to events[0] as SimpleNotifiableEvent, - "event-2" to events[1] as SimpleNotifiableEvent - ) + result shouldBeEqualTo listOf( + ProcessedType.KEEP to events[0], + ProcessedType.KEEP to events[1] ) - events shouldBeEqualTo originalEvents } @Test fun `given invites are auto accepted when processing then remove invitations`() { autoAcceptInvites._isEnabled = true - val events = mutableListOf( + val events = listOf( anInviteNotifiableEvent(roomId = "room-1"), anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo aProcessedNotificationEvents( - invitationEvents = mapOf( - "room-1" to null, - "room-2" to null - ) + result shouldBeEqualTo listOf( + ProcessedType.REMOVE to events[0], + ProcessedType.REMOVE to events[1] ) - events shouldBeEqualTo emptyList() } @Test - fun `given invites are not auto accepted when processing then return without mutating`() { + fun `given invites are not auto accepted when processing then keep invitation events`() { autoAcceptInvites._isEnabled = false - val (events, originalEvents) = createEventsList( + val events = listOf( anInviteNotifiableEvent(roomId = "room-1"), anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo aProcessedNotificationEvents( - invitationEvents = mapOf( - "room-1" to originalEvents[0] as InviteNotifiableEvent, - "room-2" to originalEvents[1] as InviteNotifiableEvent - ) + result shouldBeEqualTo listOf( + ProcessedType.KEEP to events[0], + ProcessedType.KEEP to events[1] ) - events shouldBeEqualTo originalEvents } @Test - fun `given out of date message event when processing then removes message`() { - val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) + fun `given out of date message event when processing then removes message event`() { + val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsOutOfDate(events[0]) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo aProcessedNotificationEvents( - roomEvents = mapOf( - "room-1" to emptyList() - ) + result shouldBeEqualTo listOf( + ProcessedType.REMOVE to events[0], ) - events shouldBeEqualTo emptyList() } @Test - fun `given in date message event when processing then without mutating`() { - val (events, originalEvents) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) + fun `given in date message event when processing then keep message event`() { + val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsInDate(events[0]) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEventsList) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo aProcessedNotificationEvents( - roomEvents = mapOf( - "room-1" to listOf(events[0] as NotifiableMessageEvent) - ) + result shouldBeEqualTo listOf( + ProcessedType.KEEP to events[0], ) - events shouldBeEqualTo originalEvents } @Test fun `given viewing the same room as message event when processing then removes message`() { - val (events) = createEventsList(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) + val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) - val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEventsList = renderedEventsList) + val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEventsList = emptyList()) - result shouldBeEqualTo aProcessedNotificationEvents( - roomEvents = mapOf( - "room-1" to emptyList() - ) + result shouldBeEqualTo listOf( + ProcessedType.REMOVE to events[0], + ) + } + + @Test + fun `given events are different to rendered events when processing then removes difference`() { + val events = listOf(aSimpleNotifiableEvent(eventId = "event-1")) + val renderedEvents = listOf( + ProcessedType.KEEP to events[0], + ProcessedType.KEEP to anInviteNotifiableEvent(roomId = "event-2") + ) + + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEvents) + + result shouldBeEqualTo listOf( + ProcessedType.REMOVE to renderedEvents[1].second, + ProcessedType.KEEP to renderedEvents[0].second ) - events shouldBeEqualTo emptyList() } } -fun createEventsList(vararg event: NotifiableEvent): Pair, List> { - val mutableEvents = mutableListOf(*event) - val immutableEvents = mutableEvents.toList() - return mutableEvents to immutableEvents -} - -fun aProcessedNotificationEvents(simpleEvents: Map = emptyMap(), - invitationEvents: Map = emptyMap(), - roomEvents: Map> = emptyMap() -) = GroupedNotificationEvents( - roomEvents = roomEvents, - simpleEvents = simpleEvents, - invitationEvents = invitationEvents, -) - fun aSimpleNotifiableEvent(eventId: String) = SimpleNotifiableEvent( matrixID = null, eventId = eventId, diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index fc20f09811..98684f278d 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -46,7 +46,7 @@ class NotificationFactoryTest { @Test fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) { val expectedNotification = notificationUtils.givenBuildRoomInvitationNotificationFor(AN_INVITATION_EVENT, MY_USER_ID) - val roomInvitation = mapOf(A_ROOM_ID to AN_INVITATION_EVENT) + val roomInvitation = listOf(ProcessedType.KEEP to AN_INVITATION_EVENT) val result = roomInvitation.toNotifications(MY_USER_ID) @@ -63,7 +63,7 @@ class NotificationFactoryTest { @Test fun `given a missing event in room invitation when mapping to notification then is Removed`() = testWith(notificationFactory) { - val missingEventRoomInvitation: Map = mapOf(A_ROOM_ID to null) + val missingEventRoomInvitation = listOf(ProcessedType.REMOVE to AN_INVITATION_EVENT) val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) @@ -75,7 +75,7 @@ class NotificationFactoryTest { @Test fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) { val expectedNotification = notificationUtils.givenBuildSimpleInvitationNotificationFor(A_SIMPLE_EVENT, MY_USER_ID) - val roomInvitation = mapOf(AN_EVENT_ID to A_SIMPLE_EVENT) + val roomInvitation = listOf(ProcessedType.KEEP to A_SIMPLE_EVENT) val result = roomInvitation.toNotifications(MY_USER_ID) @@ -92,7 +92,7 @@ class NotificationFactoryTest { @Test fun `given a missing simple event when mapping to notification then is Removed`() = testWith(notificationFactory) { - val missingEventRoomInvitation: Map = mapOf(AN_EVENT_ID to null) + val missingEventRoomInvitation = listOf(ProcessedType.REMOVE to A_SIMPLE_EVENT) val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index bd0d1e8d3f..4f65c3861a 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -29,8 +29,8 @@ private const val AN_EVENT_ID = "event-id" private const val A_ROOM_ID = "room-id" private const val USE_COMPLETE_NOTIFICATION_FORMAT = true -private val AN_EVENT_LIST = mapOf() -private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyMap(), emptyMap()) +private val AN_EVENT_LIST = listOf>() +private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyList(), emptyList()) private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk()) private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed private val A_NOTIFICATION = mockk() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt index cc6f84f813..a6e7d1a078 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationFactory.kt @@ -16,9 +16,9 @@ package im.vector.app.test.fakes +import im.vector.app.features.notifications.GroupedNotificationEvents import im.vector.app.features.notifications.NotificationFactory import im.vector.app.features.notifications.OneShotNotification -import im.vector.app.features.notifications.GroupedNotificationEvents import im.vector.app.features.notifications.RoomNotification import im.vector.app.features.notifications.SummaryNotification import io.mockk.every @@ -50,7 +50,6 @@ class FakeNotificationFactory { useCompleteNotificationFormat ) } returns summaryNotification - } } } From c67b9ee81e3f34eaad3bd6d2357d463816c95e2e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Oct 2021 17:04:07 +0100 Subject: [PATCH 30/49] ensuring that we remove read messages when they come through by respecting the processed type when creating the notifications --- .../app/features/notifications/NotificationFactory.kt | 11 ++++++++--- .../features/notifications/NotificationRenderer.kt | 10 +++++----- .../features/notifications/NotificationFactoryTest.kt | 7 ++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index fe1671b58b..6be18371b1 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -27,16 +27,21 @@ class NotificationFactory @Inject constructor( private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { - fun Map>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { + fun Map>>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { return map { (roomId, events) -> when { events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) - else -> roomGroupMessageCreator.createRoomMessage(events, roomId, myUserDisplayName, myUserAvatarUrl) + else -> { + val messageEvents = events.filter { it.first == ProcessedType.KEEP }.map { it.second } + roomGroupMessageCreator.createRoomMessage(messageEvents, roomId, myUserDisplayName, myUserAvatarUrl) + } } } } - private fun List.hasNoEventsToDisplay() = isEmpty() || all { it.canNotBeDisplayed() } + private fun List>.hasNoEventsToDisplay() = isEmpty() || all { + it.first == ProcessedType.REMOVE || it.second.canNotBeDisplayed() + } private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index 7cf0a8872a..ceeffd0bfa 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -34,7 +34,7 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean, - eventsToProcess: List>) { + eventsToProcess: List) { val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) @@ -108,8 +108,8 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer } } -private fun List>.groupByType(): GroupedNotificationEvents { - val roomIdToEventMap: MutableMap> = LinkedHashMap() +private fun List.groupByType(): GroupedNotificationEvents { + val roomIdToEventMap: MutableMap>> = LinkedHashMap() val simpleEvents: MutableList> = ArrayList() val invitationEvents: MutableList> = ArrayList() forEach { @@ -117,7 +117,7 @@ private fun List>.groupByType(): GroupedNot is InviteNotifiableEvent -> invitationEvents.add(it.asPair()) is NotifiableMessageEvent -> { val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() } - roomEvents.add(event) + roomEvents.add(it.asPair()) } is SimpleNotifiableEvent -> simpleEvents.add(it.asPair()) } @@ -129,7 +129,7 @@ private fun List>.groupByType(): GroupedNot private fun Pair.asPair(): Pair = this as Pair data class GroupedNotificationEvents( - val roomEvents: Map>, + val roomEvents: Map>>, val simpleEvents: List>, val invitationEvents: List> ) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index 98684f278d..84f59dcc21 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -105,7 +105,7 @@ class NotificationFactoryTest { fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) { val events = listOf(A_MESSAGE_EVENT) val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(events, A_ROOM_ID, MY_USER_ID, MY_AVATAR_URL) - val roomWithMessage = mapOf(A_ROOM_ID to events) + val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedType.KEEP to A_MESSAGE_EVENT)) val result = roomWithMessage.toNotifications(MY_USER_ID, MY_AVATAR_URL) @@ -114,7 +114,8 @@ class NotificationFactoryTest { @Test fun `given a room with no events to display when mapping to notification then is Empty`() = testWith(notificationFactory) { - val emptyRoom: Map> = mapOf(A_ROOM_ID to emptyList()) + val events = listOf(ProcessedType.REMOVE to A_MESSAGE_EVENT) + val emptyRoom = mapOf(A_ROOM_ID to events) val result = emptyRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) @@ -125,7 +126,7 @@ class NotificationFactoryTest { @Test fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { - val redactedRoom = mapOf(A_ROOM_ID to listOf(A_MESSAGE_EVENT.copy(isRedacted = true))) + val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedType.KEEP to A_MESSAGE_EVENT.copy(isRedacted = true))) val result = redactedRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) From 4bbb637ace4ab2ae0c77bb5d81c1d636def32a65 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 13 Oct 2021 11:21:26 +0100 Subject: [PATCH 31/49] adding documentation around the two notifiable event lists which act as our notification source of truth --- .../notifications/NotificationDrawerManager.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index b7bb20237c..0fb9739331 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -55,7 +55,21 @@ class NotificationDrawerManager @Inject constructor(private val context: Context backgroundHandler = Handler(handlerThread.looper) } + /** + * The notifiable events to render + * this is our source of truth for notifications, any changes to this list will be rendered as notifications + * when events are removed the previously rendered notifications will be cancelled + * when adding or updating, the notifications will be notified + * + * Events are unique by their properties, we should be careful not to insert multiple events with the same event-id + */ private val eventList = loadEventInfo() + + /** + * The last known rendered notifiable events + * we keep track of them in order to know which events have been removed from the eventList + * allowing us to cancel any notifications previous displayed by now removed events + */ private var renderedEventsList = emptyList>() private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentRoomId: String? = null From 9fa09def96b4a34033e1e0bc4ab2645cdbe74ef6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 13 Oct 2021 11:26:53 +0100 Subject: [PATCH 32/49] fixing line lengths --- .../features/notifications/NotifiableEventProcessor.kt | 2 +- .../features/notifications/NotificationDrawerManager.kt | 8 ++++++-- .../app/features/notifications/NotificationFactory.kt | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index 88dc455e20..1acfcfc7ec 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -26,7 +26,7 @@ class NotifiableEventProcessor @Inject constructor( private val autoAcceptInvites: AutoAcceptInvites ) { - fun process(eventList: List, currentRoomId: String?, renderedEventsList: List>): List { + fun process(eventList: List, currentRoomId: String?, renderedEventsList: List): List { val processedEventList = eventList.map { when (it) { is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 0fb9739331..3db1ab1f84 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -262,8 +262,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val user = session.getUser(session.myUserId) // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash val myUserDisplayName = user?.toMatrixItem()?.getBestName() ?: session.myUserId - val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE) - + val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail( + contentUrl = user?.avatarUrl, + width = avatarSize, + height = avatarSize, + method = ContentUrlResolver.ThumbnailMethod.SCALE + ) notificationRenderer.render(session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventsToRender) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 6be18371b1..019b37d61a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -21,13 +21,15 @@ import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import javax.inject.Inject +private typealias ProcessedMessageEvent = Pair + class NotificationFactory @Inject constructor( private val notificationUtils: NotificationUtils, private val roomGroupMessageCreator: RoomGroupMessageCreator, private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { - fun Map>>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { + fun Map>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { return map { (roomId, events) -> when { events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) From 86ce6a404ec26f09781c4e668434a48b74ffd9bf Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 13 Oct 2021 11:30:29 +0100 Subject: [PATCH 33/49] adding missing fixture parameter from rebase --- .../app/features/notifications/NotifiableEventProcessorTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index 1c0f5f9390..c85d01a603 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -147,6 +147,7 @@ fun anInviteNotifiableEvent(roomId: String) = InviteNotifiableEvent( matrixID = null, eventId = "event-id", roomId = roomId, + roomName = "a room name", editedEventId = null, noisy = false, title = "title", From 4748a385ea7f3081b3edf28e56f7b62e7d98e636 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 13 Oct 2021 11:32:48 +0100 Subject: [PATCH 34/49] inlining single use extension functions --- .../app/features/notifications/NotificationFactory.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 019b37d61a..afea56c5b9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -86,9 +86,9 @@ class NotificationFactory @Inject constructor( invitationNotifications: List, simpleNotifications: List, useCompleteNotificationFormat: Boolean): SummaryNotification { - val roomMeta = roomNotifications.mapToMeta() - val invitationMeta = invitationNotifications.mapToMeta() - val simpleMeta = simpleNotifications.mapToMeta() + val roomMeta = roomNotifications.filterIsInstance().map { it.meta } + val invitationMeta = invitationNotifications.filterIsInstance().map { it.meta } + val simpleMeta = simpleNotifications.filterIsInstance().map { it.meta } return when { roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed else -> SummaryNotification.Update( @@ -102,11 +102,6 @@ class NotificationFactory @Inject constructor( } } -private fun List.mapToMeta() = filterIsInstance().map { it.meta } - -@JvmName("mapToMetaOneShotNotification") -private fun List.mapToMeta() = filterIsInstance().map { it.meta } - sealed interface RoomNotification { data class Removed(val roomId: String) : RoomNotification data class Message(val notification: Notification, val shortcutInfo: ShortcutInfoCompat?, val meta: Meta) : RoomNotification { From c16e3e09e611b3e350c65b51b1054f48f9cc6f48 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 14 Oct 2021 16:56:15 +0100 Subject: [PATCH 35/49] adding missing parameter from rebase and removing no longer needed singleton annotation --- .../app/features/notifications/NotificationRenderer.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index ceeffd0bfa..ea54fdaf92 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -15,6 +15,7 @@ */ package im.vector.app.features.notifications +import android.content.Context import androidx.annotation.WorkerThread import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID @@ -23,11 +24,10 @@ import im.vector.app.features.notifications.NotificationDrawerManager.Companion. import androidx.core.content.pm.ShortcutManagerCompat import timber.log.Timber import javax.inject.Inject -import javax.inject.Singleton -@Singleton class NotificationRenderer @Inject constructor(private val notificationDisplayer: NotificationDisplayer, - private val notificationFactory: NotificationFactory) { + private val notificationFactory: NotificationFactory, + private val appContext: Context) { @WorkerThread fun render(myUserId: String, From a6e47d8b85d64f91a9ac0fb60323c1e79a48553f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 14 Oct 2021 16:58:31 +0100 Subject: [PATCH 36/49] replacing notification utils usage with the displayer and removing unused method --- .../features/notifications/NotificationDrawerManager.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 3db1ab1f84..f324318988 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -41,7 +41,7 @@ import javax.inject.Singleton */ @Singleton class NotificationDrawerManager @Inject constructor(private val context: Context, - private val notificationUtils: NotificationUtils, + private val notificationDisplayer: NotificationDisplayer, private val vectorPreferences: VectorPreferences, private val activeSessionDataSource: ActiveSessionDataSource, private val notifiableEventProcessor: NotifiableEventProcessor, @@ -243,7 +243,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val newSettings = vectorPreferences.useCompleteNotificationFormat() if (newSettings != useCompleteNotificationFormat) { // Settings has changed, remove all current notifications - notificationUtils.cancelAllNotifications() + notificationDisplayer.cancelAllNotifications() useCompleteNotificationFormat = newSettings } @@ -318,10 +318,6 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - fun displayDiagnosticNotification() { - notificationUtils.displayDiagnosticNotification() - } - companion object { const val SUMMARY_NOTIFICATION_ID = 0 const val ROOM_MESSAGES_NOTIFICATION_ID = 1 From 6fb7faa3604eb05b8a17abd812ce3026af933f6a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 14 Oct 2021 16:59:32 +0100 Subject: [PATCH 37/49] removing unused imports --- .../im/vector/app/features/notifications/NotificationFactory.kt | 1 - .../vector/app/features/notifications/NotificationRenderer.kt | 2 +- .../app/features/notifications/RoomGroupMessageCreator.kt | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index afea56c5b9..288268a00c 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -18,7 +18,6 @@ package im.vector.app.features.notifications import android.app.Notification import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat import javax.inject.Inject private typealias ProcessedMessageEvent = Pair diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index ea54fdaf92..e72b9c7110 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -17,11 +17,11 @@ package im.vector.app.features.notifications import android.content.Context import androidx.annotation.WorkerThread +import androidx.core.content.pm.ShortcutManagerCompat import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.SUMMARY_NOTIFICATION_ID -import androidx.core.content.pm.ShortcutManagerCompat import timber.log.Timber import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 113dc21ebd..35a1c12d0c 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -22,7 +22,6 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.Person import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import im.vector.app.R import im.vector.app.core.resources.StringProvider From 63090ef681e93aa2cff6de5b3604f662b0de931c Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 14 Oct 2021 17:19:14 +0100 Subject: [PATCH 38/49] updating tests with shortcut placement changes --- .../app/features/notifications/NotificationRendererTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index 4f65c3861a..c086a3ee4d 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -17,6 +17,7 @@ package im.vector.app.features.notifications import android.app.Notification +import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeNotificationDisplayer import im.vector.app.test.fakes.FakeNotificationFactory import io.mockk.mockk @@ -41,12 +42,14 @@ private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", sum class NotificationRendererTest { + private val context = FakeContext() private val notificationDisplayer = FakeNotificationDisplayer() private val notificationFactory = FakeNotificationFactory() private val notificationRenderer = NotificationRenderer( notificationDisplayer = notificationDisplayer.instance, - notificationFactory = notificationFactory.instance + notificationFactory = notificationFactory.instance, + appContext = context.instance ) @Test @@ -87,6 +90,7 @@ class NotificationRendererTest { fun `given a room message group notification is added when rendering then show the message notification and update summary`() { givenNotifications(roomNotifications = listOf(RoomNotification.Message( A_NOTIFICATION, + shortcutInfo = null, MESSAGE_META ))) From d3234b33d37c1b5208a9b3eb653ce70ce215d286 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 14 Oct 2021 17:42:47 +0100 Subject: [PATCH 39/49] increase enum class allowance by 1 --- tools/check/forbidden_strings_in_code.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 8f8625fe1c..29077c3a76 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===106 +enum class===107 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 From d1f6db4236aab4e24e6ba978e1bdc842c21499ab Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 19 Oct 2021 11:38:34 +0100 Subject: [PATCH 40/49] using dedicated ProcessedEvent data class instead of type alias for passing around the process notificatiable events - also includes @JvmName on all conflicting extensions for consistency --- .../notifications/NotifiableEventProcessor.kt | 17 ++++--- .../NotificationDrawerManager.kt | 2 +- .../notifications/NotificationFactory.kt | 23 ++++----- .../notifications/NotificationRenderer.kt | 26 +++++----- .../features/notifications/ProcessedEvent.kt | 14 ++++-- .../NotifiableEventProcessorTest.kt | 47 ++++++++++--------- .../notifications/NotificationFactoryTest.kt | 15 +++--- .../notifications/NotificationRendererTest.kt | 2 +- 8 files changed, 80 insertions(+), 66 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index 1acfcfc7ec..338c3d58eb 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -17,29 +17,32 @@ package im.vector.app.features.notifications import im.vector.app.features.invite.AutoAcceptInvites -import im.vector.app.features.notifications.ProcessedType.KEEP -import im.vector.app.features.notifications.ProcessedType.REMOVE +import im.vector.app.features.notifications.ProcessedEvent.Type.KEEP +import im.vector.app.features.notifications.ProcessedEvent.Type.REMOVE import javax.inject.Inject +private typealias ProcessedEvents = List> + class NotifiableEventProcessor @Inject constructor( private val outdatedDetector: OutdatedEventDetector, private val autoAcceptInvites: AutoAcceptInvites ) { - fun process(eventList: List, currentRoomId: String?, renderedEventsList: List): List { + fun process(eventList: List, currentRoomId: String?, renderedEventsList: ProcessedEvents): ProcessedEvents { val processedEventList = eventList.map { - when (it) { + val type = when (it) { is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) || outdatedDetector.isMessageOutdated(it)) { REMOVE } else KEEP is SimpleNotifiableEvent -> KEEP - } to it + } + ProcessedEvent(type, it) } val removedEventsDiff = renderedEventsList.filter { renderedEvent -> - eventList.none { it.eventId == renderedEvent.second.eventId } - }.map { REMOVE to it.second } + eventList.none { it.eventId == renderedEvent.event.eventId } + }.map { ProcessedEvent(REMOVE, it.event) } return removedEventsDiff + processedEventList } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index f324318988..5d2212ba1e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -70,7 +70,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context * we keep track of them in order to know which events have been removed from the eventList * allowing us to cancel any notifications previous displayed by now removed events */ - private var renderedEventsList = emptyList>() + private var renderedEventsList = emptyList>() private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentRoomId: String? = null diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 288268a00c..5dff009cec 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -20,7 +20,7 @@ import android.app.Notification import androidx.core.content.pm.ShortcutInfoCompat import javax.inject.Inject -private typealias ProcessedMessageEvent = Pair +private typealias ProcessedMessageEvents = List> class NotificationFactory @Inject constructor( private val notificationUtils: NotificationUtils, @@ -28,29 +28,30 @@ class NotificationFactory @Inject constructor( private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { - fun Map>.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { + fun Map.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { return map { (roomId, events) -> when { events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) else -> { - val messageEvents = events.filter { it.first == ProcessedType.KEEP }.map { it.second } + val messageEvents = events.onlyKeptEvents() roomGroupMessageCreator.createRoomMessage(messageEvents, roomId, myUserDisplayName, myUserAvatarUrl) } } } } - private fun List>.hasNoEventsToDisplay() = isEmpty() || all { - it.first == ProcessedType.REMOVE || it.second.canNotBeDisplayed() + private fun ProcessedMessageEvents.hasNoEventsToDisplay() = isEmpty() || all { + it.type == ProcessedEvent.Type.REMOVE || it.event.canNotBeDisplayed() } private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted - fun List>.toNotifications(myUserId: String): List { + @JvmName("toNotificationsInviteNotifiableEvent") + fun List>.toNotifications(myUserId: String): List { return map { (processed, event) -> when (processed) { - ProcessedType.REMOVE -> OneShotNotification.Removed(key = event.roomId) - ProcessedType.KEEP -> OneShotNotification.Append( + ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId) + ProcessedEvent.Type.KEEP -> OneShotNotification.Append( notificationUtils.buildRoomInvitationNotification(event, myUserId), OneShotNotification.Append.Meta( key = event.roomId, @@ -64,11 +65,11 @@ class NotificationFactory @Inject constructor( } @JvmName("toNotificationsSimpleNotifiableEvent") - fun List>.toNotifications(myUserId: String): List { + fun List>.toNotifications(myUserId: String): List { return map { (processed, event) -> when (processed) { - ProcessedType.REMOVE -> OneShotNotification.Removed(key = event.eventId) - ProcessedType.KEEP -> OneShotNotification.Append( + ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId) + ProcessedEvent.Type.KEEP -> OneShotNotification.Append( notificationUtils.buildSimpleEventNotification(event, myUserId), OneShotNotification.Append.Meta( key = event.eventId, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index e72b9c7110..5afff89402 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -34,7 +34,7 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer myUserDisplayName: String, myUserAvatarUrl: String?, useCompleteNotificationFormat: Boolean, - eventsToProcess: List) { + eventsToProcess: List>) { val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) @@ -108,28 +108,28 @@ class NotificationRenderer @Inject constructor(private val notificationDisplayer } } -private fun List.groupByType(): GroupedNotificationEvents { - val roomIdToEventMap: MutableMap>> = LinkedHashMap() - val simpleEvents: MutableList> = ArrayList() - val invitationEvents: MutableList> = ArrayList() +private fun List>.groupByType(): GroupedNotificationEvents { + val roomIdToEventMap: MutableMap>> = LinkedHashMap() + val simpleEvents: MutableList> = ArrayList() + val invitationEvents: MutableList> = ArrayList() forEach { - when (val event = it.second) { - is InviteNotifiableEvent -> invitationEvents.add(it.asPair()) + when (val event = it.event) { + is InviteNotifiableEvent -> invitationEvents.add(it.castedToEventType()) is NotifiableMessageEvent -> { val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() } - roomEvents.add(it.asPair()) + roomEvents.add(it.castedToEventType()) } - is SimpleNotifiableEvent -> simpleEvents.add(it.asPair()) + is SimpleNotifiableEvent -> simpleEvents.add(it.castedToEventType()) } } return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) } @Suppress("UNCHECKED_CAST") -private fun Pair.asPair(): Pair = this as Pair +private fun ProcessedEvent.castedToEventType(): ProcessedEvent = this as ProcessedEvent data class GroupedNotificationEvents( - val roomEvents: Map>>, - val simpleEvents: List>, - val invitationEvents: List> + val roomEvents: Map>>, + val simpleEvents: List>, + val invitationEvents: List> ) diff --git a/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt index 0901757d02..7c58c81f46 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt @@ -16,11 +16,15 @@ package im.vector.app.features.notifications -typealias ProcessedEvent = Pair +data class ProcessedEvent( + val type: Type, + val event: T +) { -enum class ProcessedType { - KEEP, - REMOVE + enum class Type { + KEEP, + REMOVE + } } -fun List.onlyKeptEvents() = filter { it.first == ProcessedType.KEEP }.map { it.second } +fun List>.onlyKeptEvents() = filter { it.type == ProcessedEvent.Type.KEEP }.map { it.event } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index c85d01a603..342567753c 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -16,6 +16,7 @@ package im.vector.app.features.notifications +import im.vector.app.features.notifications.ProcessedEvent.Type import im.vector.app.test.fakes.FakeAutoAcceptInvites import im.vector.app.test.fakes.FakeOutdatedEventDetector import org.amshove.kluent.shouldBeEqualTo @@ -39,9 +40,9 @@ class NotifiableEventProcessorTest { val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo listOf( - ProcessedType.KEEP to events[0], - ProcessedType.KEEP to events[1] + result shouldBeEqualTo listOfProcessedEvents( + Type.KEEP to events[0], + Type.KEEP to events[1] ) } @@ -55,9 +56,9 @@ class NotifiableEventProcessorTest { val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo listOf( - ProcessedType.REMOVE to events[0], - ProcessedType.REMOVE to events[1] + result shouldBeEqualTo listOfProcessedEvents( + Type.REMOVE to events[0], + Type.REMOVE to events[1] ) } @@ -71,9 +72,9 @@ class NotifiableEventProcessorTest { val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo listOf( - ProcessedType.KEEP to events[0], - ProcessedType.KEEP to events[1] + result shouldBeEqualTo listOfProcessedEvents( + Type.KEEP to events[0], + Type.KEEP to events[1] ) } @@ -84,8 +85,8 @@ class NotifiableEventProcessorTest { val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo listOf( - ProcessedType.REMOVE to events[0], + result shouldBeEqualTo listOfProcessedEvents( + Type.REMOVE to events[0], ) } @@ -96,8 +97,8 @@ class NotifiableEventProcessorTest { val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) - result shouldBeEqualTo listOf( - ProcessedType.KEEP to events[0], + result shouldBeEqualTo listOfProcessedEvents( + Type.KEEP to events[0], ) } @@ -107,26 +108,30 @@ class NotifiableEventProcessorTest { val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEventsList = emptyList()) - result shouldBeEqualTo listOf( - ProcessedType.REMOVE to events[0], + result shouldBeEqualTo listOfProcessedEvents( + Type.REMOVE to events[0], ) } @Test fun `given events are different to rendered events when processing then removes difference`() { val events = listOf(aSimpleNotifiableEvent(eventId = "event-1")) - val renderedEvents = listOf( - ProcessedType.KEEP to events[0], - ProcessedType.KEEP to anInviteNotifiableEvent(roomId = "event-2") + val renderedEvents = listOf>( + ProcessedEvent(Type.KEEP, events[0]), + ProcessedEvent(Type.KEEP, anInviteNotifiableEvent(roomId = "event-2")) ) val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEvents) - result shouldBeEqualTo listOf( - ProcessedType.REMOVE to renderedEvents[1].second, - ProcessedType.KEEP to renderedEvents[0].second + result shouldBeEqualTo listOfProcessedEvents( + Type.REMOVE to renderedEvents[1].event, + Type.KEEP to renderedEvents[0].event ) } + + private fun listOfProcessedEvents(vararg event: Pair) = event.map { + ProcessedEvent(it.first, it.second) + } } fun aSimpleNotifiableEvent(eventId: String) = SimpleNotifiableEvent( diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index 84f59dcc21..d3d48630c9 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -16,6 +16,7 @@ package im.vector.app.features.notifications +import im.vector.app.features.notifications.ProcessedEvent.Type import im.vector.app.test.fakes.FakeNotificationUtils import im.vector.app.test.fakes.FakeRoomGroupMessageCreator import im.vector.app.test.fakes.FakeSummaryGroupMessageCreator @@ -46,7 +47,7 @@ class NotificationFactoryTest { @Test fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) { val expectedNotification = notificationUtils.givenBuildRoomInvitationNotificationFor(AN_INVITATION_EVENT, MY_USER_ID) - val roomInvitation = listOf(ProcessedType.KEEP to AN_INVITATION_EVENT) + val roomInvitation = listOf(ProcessedEvent(Type.KEEP, AN_INVITATION_EVENT)) val result = roomInvitation.toNotifications(MY_USER_ID) @@ -63,7 +64,7 @@ class NotificationFactoryTest { @Test fun `given a missing event in room invitation when mapping to notification then is Removed`() = testWith(notificationFactory) { - val missingEventRoomInvitation = listOf(ProcessedType.REMOVE to AN_INVITATION_EVENT) + val missingEventRoomInvitation = listOf(ProcessedEvent(Type.REMOVE, AN_INVITATION_EVENT)) val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) @@ -75,7 +76,7 @@ class NotificationFactoryTest { @Test fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) { val expectedNotification = notificationUtils.givenBuildSimpleInvitationNotificationFor(A_SIMPLE_EVENT, MY_USER_ID) - val roomInvitation = listOf(ProcessedType.KEEP to A_SIMPLE_EVENT) + val roomInvitation = listOf(ProcessedEvent(Type.KEEP, A_SIMPLE_EVENT)) val result = roomInvitation.toNotifications(MY_USER_ID) @@ -92,7 +93,7 @@ class NotificationFactoryTest { @Test fun `given a missing simple event when mapping to notification then is Removed`() = testWith(notificationFactory) { - val missingEventRoomInvitation = listOf(ProcessedType.REMOVE to A_SIMPLE_EVENT) + val missingEventRoomInvitation = listOf(ProcessedEvent(Type.REMOVE, A_SIMPLE_EVENT)) val result = missingEventRoomInvitation.toNotifications(MY_USER_ID) @@ -105,7 +106,7 @@ class NotificationFactoryTest { fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) { val events = listOf(A_MESSAGE_EVENT) val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(events, A_ROOM_ID, MY_USER_ID, MY_AVATAR_URL) - val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedType.KEEP to A_MESSAGE_EVENT)) + val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT))) val result = roomWithMessage.toNotifications(MY_USER_ID, MY_AVATAR_URL) @@ -114,7 +115,7 @@ class NotificationFactoryTest { @Test fun `given a room with no events to display when mapping to notification then is Empty`() = testWith(notificationFactory) { - val events = listOf(ProcessedType.REMOVE to A_MESSAGE_EVENT) + val events = listOf(ProcessedEvent(Type.REMOVE, A_MESSAGE_EVENT)) val emptyRoom = mapOf(A_ROOM_ID to events) val result = emptyRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) @@ -126,7 +127,7 @@ class NotificationFactoryTest { @Test fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { - val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedType.KEEP to A_MESSAGE_EVENT.copy(isRedacted = true))) + val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)))) val result = redactedRoom.toNotifications(MY_USER_ID, MY_AVATAR_URL) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt index c086a3ee4d..f726ff1b54 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationRendererTest.kt @@ -30,7 +30,7 @@ private const val AN_EVENT_ID = "event-id" private const val A_ROOM_ID = "room-id" private const val USE_COMPLETE_NOTIFICATION_FORMAT = true -private val AN_EVENT_LIST = listOf>() +private val AN_EVENT_LIST = listOf>() private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyList(), emptyList()) private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk()) private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed From 743a71c78da6be0f23f18f5ee94984b725d72439 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 20 Oct 2021 09:42:19 +0100 Subject: [PATCH 41/49] renaming event lists to give more context and remove the list suffix/inconsistencies --- .../notifications/NotifiableEventProcessor.kt | 10 ++-- .../NotificationDrawerManager.kt | 52 +++++++++---------- .../NotifiableEventProcessorTest.kt | 14 ++--- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index 338c3d58eb..858df81bf6 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -28,8 +28,8 @@ class NotifiableEventProcessor @Inject constructor( private val autoAcceptInvites: AutoAcceptInvites ) { - fun process(eventList: List, currentRoomId: String?, renderedEventsList: ProcessedEvents): ProcessedEvents { - val processedEventList = eventList.map { + fun process(queuedEvents: List, currentRoomId: String?, renderedEvents: ProcessedEvents): ProcessedEvents { + val processedEvents = queuedEvents.map { val type = when (it) { is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) || outdatedDetector.isMessageOutdated(it)) { @@ -40,11 +40,11 @@ class NotifiableEventProcessor @Inject constructor( ProcessedEvent(type, it) } - val removedEventsDiff = renderedEventsList.filter { renderedEvent -> - eventList.none { it.eventId == renderedEvent.event.eventId } + val removedEventsDiff = renderedEvents.filter { renderedEvent -> + queuedEvents.none { it.eventId == renderedEvent.event.eventId } }.map { ProcessedEvent(REMOVE, it.event) } - return removedEventsDiff + processedEventList + return removedEventsDiff + processedEvents } private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 5d2212ba1e..c052de650e 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -63,14 +63,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context * * Events are unique by their properties, we should be careful not to insert multiple events with the same event-id */ - private val eventList = loadEventInfo() + private val queuedEvents = loadEventInfo() /** * The last known rendered notifiable events * we keep track of them in order to know which events have been removed from the eventList * allowing us to cancel any notifications previous displayed by now removed events */ - private var renderedEventsList = emptyList>() + private var renderedEvents = emptyList>() private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentRoomId: String? = null @@ -105,8 +105,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } else { Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") } - synchronized(eventList) { - val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId } + synchronized(queuedEvents) { + val existing = queuedEvents.firstOrNull { it.eventId == notifiableEvent.eventId } if (existing != null) { if (existing.canBeReplaced) { // Use the event coming from the event stream as it may contains more info than @@ -117,8 +117,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // Use setOnlyAlertOnce to ensure update notification does not interfere with sound // from first notify invocation as outlined in: // https://developer.android.com/training/notify-user/build-notification#Updating - eventList.remove(existing) - eventList.add(notifiableEvent) + queuedEvents.remove(existing) + queuedEvents.add(notifiableEvent) } else { // keep the existing one, do not replace } @@ -126,7 +126,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // Check if this is an edit if (notifiableEvent.editedEventId != null) { // This is an edition - val eventBeforeEdition = eventList.firstOrNull { + val eventBeforeEdition = queuedEvents.firstOrNull { // Edition of an event it.eventId == notifiableEvent.editedEventId || // or edition of an edition @@ -135,9 +135,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context if (eventBeforeEdition != null) { // Replace the existing notification with the new content - eventList.remove(eventBeforeEdition) + queuedEvents.remove(eventBeforeEdition) - eventList.add(notifiableEvent) + queuedEvents.add(notifiableEvent) } else { // Ignore an edit of a not displayed event in the notification drawer } @@ -148,7 +148,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context Timber.d("onNotifiableEventReceived(): skipping event, already seen") } else { seenEventIds.put(notifiableEvent.eventId) - eventList.add(notifiableEvent) + queuedEvents.add(notifiableEvent) } } } @@ -156,8 +156,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } fun onEventRedacted(eventId: String) { - synchronized(eventList) { - eventList.replace(eventId) { + synchronized(queuedEvents) { + queuedEvents.replace(eventId) { when (it) { is InviteNotifiableEvent -> it.copy(isRedacted = true) is NotifiableMessageEvent -> it.copy(isRedacted = true) @@ -171,8 +171,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context * Clear all known events and refresh the notification drawer */ fun clearAllEvents() { - synchronized(eventList) { - eventList.clear() + synchronized(queuedEvents) { + queuedEvents.clear() } refreshNotificationDrawer() } @@ -194,7 +194,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context */ fun setCurrentRoom(roomId: String?) { var hasChanged: Boolean - synchronized(eventList) { + synchronized(queuedEvents) { hasChanged = roomId != currentRoomId currentRoomId = roomId } @@ -211,8 +211,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } private fun removeAll(predicate: (NotifiableEvent) -> Boolean): Boolean { - return synchronized(eventList) { - eventList.removeAll(predicate) + return synchronized(queuedEvents) { + queuedEvents.removeAll(predicate) } } @@ -247,17 +247,17 @@ class NotificationDrawerManager @Inject constructor(private val context: Context useCompleteNotificationFormat = newSettings } - val eventsToRender = synchronized(eventList) { - notifiableEventProcessor.process(eventList, currentRoomId, renderedEventsList).also { - eventList.clear() - eventList.addAll(it.onlyKeptEvents()) + val eventsToRender = synchronized(queuedEvents) { + notifiableEventProcessor.process(queuedEvents, currentRoomId, renderedEvents).also { + queuedEvents.clear() + queuedEvents.addAll(it.onlyKeptEvents()) } } - if (renderedEventsList == eventsToRender) { + if (renderedEvents == eventsToRender) { Timber.d("Skipping notification update due to event list not changing") } else { - renderedEventsList = eventsToRender + renderedEvents = eventsToRender val session = currentSession ?: return val user = session.getUser(session.myUserId) // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash @@ -277,8 +277,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } fun persistInfo() { - synchronized(eventList) { - if (eventList.isEmpty()) { + synchronized(queuedEvents) { + if (queuedEvents.isEmpty()) { deleteCachedRoomNotifications() return } @@ -286,7 +286,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (!file.exists()) file.createNewFile() FileOutputStream(file).use { - currentSession?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it) + currentSession?.securelyStoreObject(queuedEvents, KEY_ALIAS_SECRET_STORAGE, it) } } catch (e: Throwable) { Timber.e(e, "## Failed to save cached notification info") diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index 342567753c..f6938cb4ae 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -38,7 +38,7 @@ class NotifiableEventProcessorTest { aSimpleNotifiableEvent(eventId = "event-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) result shouldBeEqualTo listOfProcessedEvents( Type.KEEP to events[0], @@ -54,7 +54,7 @@ class NotifiableEventProcessorTest { anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) result shouldBeEqualTo listOfProcessedEvents( Type.REMOVE to events[0], @@ -70,7 +70,7 @@ class NotifiableEventProcessorTest { anInviteNotifiableEvent(roomId = "room-2") ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) result shouldBeEqualTo listOfProcessedEvents( Type.KEEP to events[0], @@ -83,7 +83,7 @@ class NotifiableEventProcessorTest { val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsOutOfDate(events[0]) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) result shouldBeEqualTo listOfProcessedEvents( Type.REMOVE to events[0], @@ -95,7 +95,7 @@ class NotifiableEventProcessorTest { val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) outdatedDetector.givenEventIsInDate(events[0]) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = emptyList()) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) result shouldBeEqualTo listOfProcessedEvents( Type.KEEP to events[0], @@ -106,7 +106,7 @@ class NotifiableEventProcessorTest { fun `given viewing the same room as message event when processing then removes message`() { val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1")) - val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEventsList = emptyList()) + val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEvents = emptyList()) result shouldBeEqualTo listOfProcessedEvents( Type.REMOVE to events[0], @@ -121,7 +121,7 @@ class NotifiableEventProcessorTest { ProcessedEvent(Type.KEEP, anInviteNotifiableEvent(roomId = "event-2")) ) - val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEventsList = renderedEvents) + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = renderedEvents) result shouldBeEqualTo listOfProcessedEvents( Type.REMOVE to renderedEvents[1].event, From e8bd27e78536f2e60c678047aff56f020262fcf9 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 20 Oct 2021 15:47:11 +0100 Subject: [PATCH 42/49] adding changelog entries --- changelog.d/3395.bugfix | 1 + changelog.d/4152.bugfix | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/3395.bugfix create mode 100644 changelog.d/4152.bugfix diff --git a/changelog.d/3395.bugfix b/changelog.d/3395.bugfix new file mode 100644 index 0000000000..9482e1bc7e --- /dev/null +++ b/changelog.d/3395.bugfix @@ -0,0 +1 @@ +Fixes marking individual notifications as read causing other notifications to be dismissed \ No newline at end of file diff --git a/changelog.d/4152.bugfix b/changelog.d/4152.bugfix new file mode 100644 index 0000000000..1ff45609b5 --- /dev/null +++ b/changelog.d/4152.bugfix @@ -0,0 +1 @@ +Tentatively fixing the doubled notifications by updating the group summary at specific points in the notification rendering cycle \ No newline at end of file From c56101d2274394b8e8a3d94cc81c75de33cdaae9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Oct 2021 18:07:23 +0200 Subject: [PATCH 43/49] Do not use the room member avatar as a room avatar --- .../app/features/notifications/RoomGroupMessageCreator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 35a1c12d0c..8e8a5d5e8a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -163,7 +163,7 @@ class RoomGroupMessageCreator @Inject constructor( if (events.isEmpty()) return null // Use the last event (most recent?) - val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath + val roomAvatarPath = events.last().roomAvatarPath return bitmapLoader.getRoomBitmap(roomAvatarPath) } From 2bd2cbf84eba09773a5273aaf437ab09f7d96575 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Oct 2021 18:10:42 +0200 Subject: [PATCH 44/49] Compact code --- .../app/features/notifications/RoomGroupMessageCreator.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 8e8a5d5e8a..bdd7d026f9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -160,12 +160,10 @@ class RoomGroupMessageCreator @Inject constructor( } private fun getRoomBitmap(events: List): Bitmap? { - if (events.isEmpty()) return null - // Use the last event (most recent?) - val roomAvatarPath = events.last().roomAvatarPath - - return bitmapLoader.getRoomBitmap(roomAvatarPath) + return events.lastOrNull() + ?.roomAvatarPath + ?.let { bitmapLoader.getRoomBitmap(it) } } } From be67836a3eaea8df5276522e9bb18ce58825a0db Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Oct 2021 21:33:52 +0200 Subject: [PATCH 45/49] Tiny formatting --- .../notifications/SummaryGroupMessageCreator.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt index ddef31a0f5..91163434c2 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/SummaryGroupMessageCreator.kt @@ -67,8 +67,7 @@ class SummaryGroupMessageCreator @Inject constructor( summaryInboxStyle.setBigContentTitle(sumTitle) // TODO get latest event? .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) - return if (useCompleteNotificationFormat - ) { + return if (useCompleteNotificationFormat) { notificationUtils.buildSummaryListNotification( summaryInboxStyle, sumTitle, @@ -76,9 +75,14 @@ class SummaryGroupMessageCreator @Inject constructor( lastMessageTimestamp = lastMessageTimestamp ) } else { - processSimpleGroupSummary(summaryIsNoisy, messageCount, - simpleNotifications.size, invitationNotifications.size, - roomNotifications.size, lastMessageTimestamp) + processSimpleGroupSummary( + summaryIsNoisy, + messageCount, + simpleNotifications.size, + invitationNotifications.size, + roomNotifications.size, + lastMessageTimestamp + ) } } From b146501f2983152f935a69b6f42380c6151b24ea Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 21 Oct 2021 12:03:41 +0100 Subject: [PATCH 46/49] avoiding multiple list iterations via mapNotNull --- .../im/vector/app/features/notifications/ProcessedEvent.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt index 7c58c81f46..8bd9819ca9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/ProcessedEvent.kt @@ -27,4 +27,6 @@ data class ProcessedEvent( } } -fun List>.onlyKeptEvents() = filter { it.type == ProcessedEvent.Type.KEEP }.map { it.event } +fun List>.onlyKeptEvents() = mapNotNull { processedEvent -> + processedEvent.event.takeIf { processedEvent.type == ProcessedEvent.Type.KEEP } +} From a5fe6f7212bbf5b8bb6071793980886a7c5680dd Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 21 Oct 2021 11:31:02 +0100 Subject: [PATCH 47/49] removing redacted events from the room notification message list --- .../features/notifications/NotificationFactory.kt | 2 +- .../notifications/NotificationFactoryTest.kt | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 5dff009cec..adc4e44bcc 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -33,7 +33,7 @@ class NotificationFactory @Inject constructor( when { events.hasNoEventsToDisplay() -> RoomNotification.Removed(roomId) else -> { - val messageEvents = events.onlyKeptEvents() + val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted } roomGroupMessageCreator.createRoomMessage(messageEvents, roomId, myUserDisplayName, myUserAvatarUrl) } } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index d3d48630c9..d720881bac 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -135,6 +135,20 @@ class NotificationFactoryTest { roomId = A_ROOM_ID )) } + + @Test + fun `given a room with redacted and non redacted message events when mapping to notification then redacted events are removed`() = testWith(notificationFactory) { + val roomWithRedactedMessage = mapOf(A_ROOM_ID to listOf( + ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)), + ProcessedEvent(Type.KEEP, A_MESSAGE_EVENT.copy(eventId = "not-redacted")) + )) + val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = "not-redacted")) + val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(withRedactedRemoved, A_ROOM_ID, MY_USER_ID, MY_AVATAR_URL) + + val result = roomWithRedactedMessage.toNotifications(MY_USER_ID, MY_AVATAR_URL) + + result shouldBeEqualTo listOf(expectedNotification) + } } fun testWith(receiver: T, block: T.() -> Unit) { From 6d9877d79c156fdb2137c036e4e02995a8444f00 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 21 Oct 2021 11:51:28 +0100 Subject: [PATCH 48/49] filtering out redacted simple message events, we handle them by updating the notifications --- .../notifications/NotifiableEventProcessor.kt | 6 +++++- .../NotifiableEventProcessorTest.kt | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index 858df81bf6..3d10d74fe3 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -19,6 +19,7 @@ package im.vector.app.features.notifications import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.notifications.ProcessedEvent.Type.KEEP import im.vector.app.features.notifications.ProcessedEvent.Type.REMOVE +import org.matrix.android.sdk.api.session.events.model.EventType import javax.inject.Inject private typealias ProcessedEvents = List> @@ -35,7 +36,10 @@ class NotifiableEventProcessor @Inject constructor( is NotifiableMessageEvent -> if (shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) || outdatedDetector.isMessageOutdated(it)) { REMOVE } else KEEP - is SimpleNotifiableEvent -> KEEP + is SimpleNotifiableEvent -> when (it.type) { + EventType.REDACTION -> REMOVE + else -> KEEP + } } ProcessedEvent(type, it) } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index f6938cb4ae..229ab39d1d 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -21,6 +21,7 @@ import im.vector.app.test.fakes.FakeAutoAcceptInvites import im.vector.app.test.fakes.FakeOutdatedEventDetector import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.EventType private val NOT_VIEWING_A_ROOM: String? = null @@ -46,6 +47,17 @@ class NotifiableEventProcessorTest { ) } + @Test + fun `given redacted simple event when processing then remove redaction event`() { + val events = listOf(aSimpleNotifiableEvent(eventId = "event-1", type = EventType.REDACTION)) + + val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) + + result shouldBeEqualTo listOfProcessedEvents( + Type.REMOVE to events[0] + ) + } + @Test fun `given invites are auto accepted when processing then remove invitations`() { autoAcceptInvites._isEnabled = true @@ -134,14 +146,14 @@ class NotifiableEventProcessorTest { } } -fun aSimpleNotifiableEvent(eventId: String) = SimpleNotifiableEvent( +fun aSimpleNotifiableEvent(eventId: String, type: String? = null) = SimpleNotifiableEvent( matrixID = null, eventId = eventId, editedEventId = null, noisy = false, title = "title", description = "description", - type = null, + type = type, timestamp = 0, soundName = null, canBeReplaced = false, From 124061e1dbe5de1eecd0179e91d83f3d29868002 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 21 Oct 2021 12:27:12 +0100 Subject: [PATCH 49/49] adding changelog entry --- changelog.d/1491.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1491.bugfix diff --git a/changelog.d/1491.bugfix b/changelog.d/1491.bugfix new file mode 100644 index 0000000000..0ff6bd2c11 --- /dev/null +++ b/changelog.d/1491.bugfix @@ -0,0 +1 @@ +Stops showing a dedicated redacted event notification, the message notifications will update accordingly \ No newline at end of file