From 7c559363a4e493b8a27e9a477fcfa171f82dc857 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Jun 2024 13:05:16 +0200 Subject: [PATCH 01/14] Handle quick reply from notification (still disabled) --- .../libraries/matrix/api/core/ThreadId.kt | 2 + .../NotificationBroadcastReceiver.kt | 179 ++++++++---------- 2 files changed, 83 insertions(+), 98 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt index 09ba7f37f8..8e5845f621 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt @@ -33,3 +33,5 @@ value class ThreadId(val value: String) : Serializable { override fun toString(): String = value } + +fun ThreadId.asEventId(): EventId = EventId(value) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 0f00d552e7..b071d61482 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl.notifications import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import androidx.core.app.RemoteInput import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag @@ -26,11 +27,19 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.core.asEventId +import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived +import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber +import java.util.UUID import javax.inject.Inject private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag) @@ -44,19 +53,23 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var sessionPreferencesStore: SessionPreferencesStoreFactory @Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager @Inject lateinit var actionIds: NotificationActionIds + @Inject lateinit var systemClock: SystemClock + @Inject lateinit var onNotifiableEventReceived: OnNotifiableEventReceived override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId) + val threadId = intent.getStringExtra(KEY_THREAD_ID)?.let(::ThreadId) val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId) context.bindings().inject(this) Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") when (intent.action) { - actionIds.smartReply -> - handleSmartReply(intent, context) + actionIds.smartReply -> if (roomId != null) { + handleSmartReply(sessionId, roomId, threadId, intent, context) + } actionIds.dismissRoom -> if (roomId != null) { defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId) } @@ -104,113 +117,84 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { client.getRoom(roomId)?.markAsRead(receiptType = receiptType) } - @Suppress("UNUSED_PARAMETER") - private fun handleSmartReply(intent: Intent, context: Context) { - /* + private fun handleSmartReply( + sessionId: SessionId, + roomId: RoomId, + threadId: ThreadId?, + intent: Intent, + context: Context + ) = appCoroutineScope.launch { val message = getReplyMessage(intent) - val sessionId = intent.getStringExtra(KEY_SESSION_ID)?.let(::SessionId) - val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId) - val threadId = intent.getStringExtra(KEY_THREAD_ID)?.let(::ThreadId) - if (message.isNullOrBlank() || roomId == null) { + if (message.isNullOrBlank()) { // ignore this event // Can this happen? should we update notification? - return + return@launch } - activeSessionHolder.getActiveSession().let { session -> - session.getRoom(roomId)?.let { room -> - sendMatrixEvent(message, threadId, session, room, context) - } - } - */ - } - - /* - private fun sendMatrixEvent(message: String, threadId: String?, session: Session, room: Room, context: Context?) { - if (threadId != null) { - room.relationService().replyInThread( - rootThreadEventId = threadId, - replyInThreadText = message, + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch + client.getRoom(roomId)?.let { room -> + sendMatrixEvent( + sessionId = sessionId, + roomId = roomId, + threadId = threadId, + room = room, + message = message, + context = context, ) - } else { - room.sendService().sendTextMessage(message) } + } + private suspend fun sendMatrixEvent( + sessionId: SessionId, + roomId: RoomId, + threadId: ThreadId?, + room: MatrixRoom, + message: String, + context: Context, + ) { // Create a new event to be displayed in the notification drawer, right now - val notifiableMessageEvent = NotifiableMessageEvent( - // Generate a Fake event id - eventId = UUID.randomUUID().toString(), - editedEventId = null, - noisy = false, - timestamp = clock.epochMillis(), - senderName = session.roomService().getRoomMember(session.myUserId, room.roomId)?.displayName - ?: context?.getString(R.string.notification_sender_me), - senderId = session.myUserId, - body = message, - imageUriString = null, - roomId = room.roomId, - threadId = threadId, - roomName = room.roomSummary()?.displayName ?: room.roomId, - roomIsDirect = room.roomSummary()?.isDirect == true, - outGoingMessage = true, - canBeReplaced = false + sessionId = sessionId, + roomId = roomId, + // Generate a Fake event id + eventId = EventId("\$" + UUID.randomUUID().toString()), + editedEventId = null, + canBeReplaced = false, + senderId = sessionId, + noisy = false, + timestamp = systemClock.epochMillis(), + senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId).getOrNull()?.disambiguatedDisplayName + ?: context.getString(R.string.notification_sender_me), + body = message, + imageUriString = null, + threadId = threadId, + roomName = room.displayName, + roomIsDirect = room.isDirect, + outGoingMessage = true, ) + onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) - notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notifiableMessageEvent) } - - /* - // TODO Error cannot be managed the same way than in Riot - - val event = Event(mxMessage, session.credentials.userId, roomId) - room.storeOutgoingEvent(event) - room.sendEvent(event, object : MatrixCallback { - override fun onSuccess(info: Void?) { - Timber.v("Send message : onSuccess ") - } - - override fun onNetworkError(e: Exception) { - Timber.e(e, "Send message : onNetworkError") - onSmartReplyFailed(e.localizedMessage) - } - - override fun onMatrixError(e: MatrixError) { - Timber.v("Send message : onMatrixError " + e.message) - if (e is MXCryptoError) { - Toast.makeText(context, e.detailedErrorDescription, Toast.LENGTH_SHORT).show() - onSmartReplyFailed(e.detailedErrorDescription) - } else { - Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show() - onSmartReplyFailed(e.localizedMessage) - } - } - - override fun onUnexpectedError(e: Exception) { - Timber.e(e, "Send message : onUnexpectedError " + e.message) - onSmartReplyFailed(e.message) - } - - - fun onSmartReplyFailed(reason: String?) { - val notifiableMessageEvent = NotifiableMessageEvent( - event.eventId, - false, - clock.epochMillis(), - session.myUser?.displayname - ?: context?.getString(R.string.notification_sender_me), - session.myUserId, - message, - roomId, - room.getRoomDisplayName(context), - room.isDirect) - notifiableMessageEvent.outGoingMessage = true - notifiableMessageEvent.outGoingMessageFailed = true - - VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) - VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null) - } - }) - */ + if (threadId != null) { + room.liveTimeline.replyMessage( + eventId = threadId.asEventId(), + body = message, + htmlBody = null, + mentions = emptyList() + ) + } else { + room.liveTimeline.sendMessage( + body = message, + htmlBody = null, + mentions = emptyList() + ) + }.onFailure { + Timber.e(it, "Failed to send smart reply message") + onNotifiableEventReceived.onNotifiableEventReceived( + notifiableMessageEvent.copy( + outGoingMessageFailed = true + ) + ) + } } private fun getReplyMessage(intent: Intent?): String? { @@ -222,7 +206,6 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { } return null } - */ companion object { const val KEY_SESSION_ID = "sessionID" From 3ddec73ac5abbef054b49138a7a1cde35ee46bb4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Jun 2024 13:15:05 +0200 Subject: [PATCH 02/14] When replying from notification, do not interfere with `specialModeEventTimelineItem` --- .../MessageComposerPresenterTest.kt | 4 +-- .../libraries/matrix/api/timeline/Timeline.kt | 8 +++++- .../matrix/impl/timeline/RustTimeline.kt | 25 +++++++++++++++---- .../matrix/test/timeline/FakeTimeline.kt | 7 ++++-- .../NotificationBroadcastReceiver.kt | 3 ++- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index a9a5dfe356..1103f4f484 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -395,7 +395,7 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List -> + val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List, _: Boolean -> Result.success(Unit) } val timeline = FakeTimeline().apply { @@ -909,7 +909,7 @@ class MessageComposerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - send messages with intentional mentions`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List -> + val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List, _: Boolean -> Result.success(Unit) } val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 6d0850db83..5c1da08b65 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -57,7 +57,13 @@ interface Timeline : AutoCloseable { suspend fun enterSpecialMode(eventId: EventId?): Result - suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result + suspend fun replyMessage( + eventId: EventId, + body: String, + htmlBody: String?, + mentions: List, + fromNotification: Boolean = false, + ): Result suspend fun sendImage( file: File, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index fee898988e..d15565a3f4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -308,13 +308,28 @@ class RustTimeline( } } - override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result = withContext(dispatcher) { + override suspend fun replyMessage( + eventId: EventId, + body: String, + htmlBody: String?, + mentions: List, + fromNotification: Boolean, + ): Result = withContext(dispatcher) { runCatching { - val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value) - inReplyTo.use { eventTimelineItem -> - inner.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem) + val msg = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()) + if (fromNotification) { + // When replying from a notification, do not interfere with `specialModeEventTimelineItem` + val inReplyTo = inner.getEventTimelineItemByEventId(eventId.value) + inReplyTo.use { eventTimelineItem -> + inner.sendReply(msg, eventTimelineItem) + } + } else { + val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value) + inReplyTo.use { eventTimelineItem -> + inner.sendReply(msg, eventTimelineItem) + } + specialModeEventTimelineItem = null } - specialModeEventTimelineItem = null } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index 6ec8386651..1cb4a3c3eb 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -104,7 +104,8 @@ class FakeTimeline( body: String, htmlBody: String?, mentions: List, - ) -> Result = { _, _, _, _ -> + fromNotification: Boolean, + ) -> Result = { _, _, _, _, _ -> Result.success(Unit) } @@ -113,11 +114,13 @@ class FakeTimeline( body: String, htmlBody: String?, mentions: List, + fromNotification: Boolean, ): Result = replyMessageLambda( eventId, body, htmlBody, - mentions + mentions, + fromNotification, ) var sendImageLambda: ( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index b071d61482..6a10f49a3b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -179,7 +179,8 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { eventId = threadId.asEventId(), body = message, htmlBody = null, - mentions = emptyList() + mentions = emptyList(), + fromNotification = true, ) } else { room.liveTimeline.sendMessage( From 3b0c59e6484fa378921f7111c3581414acc6028d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Jun 2024 16:57:49 +0200 Subject: [PATCH 03/14] Fix test --- .../impl/textcomposer/MessageComposerPresenterTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index 1103f4f484..5315cd54e2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -426,7 +426,7 @@ class MessageComposerPresenterTest { assert(replyMessageLambda) .isCalledOnce() - .with(any(), value(A_REPLY), value(A_REPLY), any()) + .with(any(), value(A_REPLY), value(A_REPLY), any(), value(false)) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -956,7 +956,7 @@ class MessageComposerPresenterTest { assert(replyMessageLambda) .isCalledOnce() - .with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2)))) + .with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2))), value(false)) // Check intentional mentions on edit message skipItems(1) From 61b37099706e50063688de263b06c23f194b580f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 09:29:51 +0200 Subject: [PATCH 04/14] Let NotificationBroadcastReceiver inject NotificationDrawerManager instead of implementation --- .../notifications/NotificationDrawerManager.kt | 5 +++++ .../DefaultNotificationDrawerManager.kt | 6 +++--- .../NotificationBroadcastReceiver.kt | 17 +++++++++-------- .../FakeNotificationDrawerManager.kt | 10 ++++++++++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt index 9a778195fa..f7babb1e35 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt @@ -16,10 +16,15 @@ package io.element.android.libraries.push.api.notifications +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId interface NotificationDrawerManager { + fun clearAllMessagesEvents(sessionId: SessionId) + fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) + fun clearEvent(sessionId: SessionId, eventId: EventId) + fun clearMembershipNotificationForSession(sessionId: SessionId) fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index cfbf3c6950..03c1cd21a2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -123,7 +123,7 @@ class DefaultNotificationDrawerManager @Inject constructor( /** * Clear all known message events for a [sessionId]. */ - fun clearAllMessagesEvents(sessionId: SessionId) { + override fun clearAllMessagesEvents(sessionId: SessionId) { notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) clearSummaryNotificationIfNeeded(sessionId) } @@ -141,7 +141,7 @@ class DefaultNotificationDrawerManager @Inject constructor( * Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room. * Can also be called when a notification for this room is dismissed by the user. */ - fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { + override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) clearSummaryNotificationIfNeeded(sessionId) } @@ -164,7 +164,7 @@ class DefaultNotificationDrawerManager @Inject constructor( /** * Clear the notifications for a single event. */ - fun clearEvent(sessionId: SessionId, eventId: EventId) { + override fun clearEvent(sessionId: SessionId, eventId: EventId) { val id = notificationIdProvider.getRoomEventNotificationId(sessionId) notificationManager.cancel(eventId.value, id) clearSummaryNotificationIfNeeded(sessionId) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 6a10f49a3b..12d8bd8b42 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived @@ -51,7 +52,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var appCoroutineScope: CoroutineScope @Inject lateinit var matrixClientProvider: MatrixClientProvider @Inject lateinit var sessionPreferencesStore: SessionPreferencesStoreFactory - @Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager + @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var actionIds: NotificationActionIds @Inject lateinit var systemClock: SystemClock @Inject lateinit var onNotifiableEventReceived: OnNotifiableEventReceived @@ -71,26 +72,26 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { handleSmartReply(sessionId, roomId, threadId, intent, context) } actionIds.dismissRoom -> if (roomId != null) { - defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId) + notificationDrawerManager.clearMessagesForRoom(sessionId, roomId) } actionIds.dismissSummary -> - defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId) + notificationDrawerManager.clearAllMessagesEvents(sessionId) actionIds.dismissInvite -> if (roomId != null) { - defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) } actionIds.dismissEvent -> if (eventId != null) { - defaultNotificationDrawerManager.clearEvent(sessionId, eventId) + notificationDrawerManager.clearEvent(sessionId, eventId) } actionIds.markRoomRead -> if (roomId != null) { - defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId) + notificationDrawerManager.clearMessagesForRoom(sessionId, roomId) handleMarkAsRead(sessionId, roomId) } actionIds.join -> if (roomId != null) { - defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) handleJoinRoom(sessionId, roomId) } actionIds.reject -> if (roomId != null) { - defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) handleRejectRoom(sessionId, roomId) } } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt index 1531d2df48..9b932cd79a 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.test.notifications +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.notifications.NotificationDrawerManager @@ -24,6 +25,15 @@ class FakeNotificationDrawerManager : NotificationDrawerManager { private val clearMemberShipNotificationForSessionCallsCount = mutableMapOf() private val clearMemberShipNotificationForRoomCallsCount = mutableMapOf() + override fun clearAllMessagesEvents(sessionId: SessionId) { + } + + override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { + } + + override fun clearEvent(sessionId: SessionId, eventId: EventId) { + } + override fun clearMembershipNotificationForSession(sessionId: SessionId) { clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value } } From 37d9d42eb7541a7a543711a3aa512362d867857e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 09:41:31 +0200 Subject: [PATCH 05/14] NotificationBroadcastReceiver now delegate treatment to NotificationBroadcastReceiverHandler to be able to unit test the logic --- .../NotificationBroadcastReceiver.kt | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 12d8bd8b42..c78aa55c8b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -49,22 +49,37 @@ private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.Not * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.). */ class NotificationBroadcastReceiver : BroadcastReceiver() { - @Inject lateinit var appCoroutineScope: CoroutineScope - @Inject lateinit var matrixClientProvider: MatrixClientProvider - @Inject lateinit var sessionPreferencesStore: SessionPreferencesStoreFactory - @Inject lateinit var notificationDrawerManager: NotificationDrawerManager - @Inject lateinit var actionIds: NotificationActionIds - @Inject lateinit var systemClock: SystemClock - @Inject lateinit var onNotifiableEventReceived: OnNotifiableEventReceived + @Inject lateinit var notificationBroadcastReceiverHandler: NotificationBroadcastReceiverHandler override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return - val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return - val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId) - val threadId = intent.getStringExtra(KEY_THREAD_ID)?.let(::ThreadId) - val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId) - context.bindings().inject(this) + notificationBroadcastReceiverHandler.onReceive(context, intent) + } + + companion object { + const val KEY_SESSION_ID = "sessionID" + const val KEY_ROOM_ID = "roomID" + const val KEY_THREAD_ID = "threadID" + const val KEY_EVENT_ID = "eventID" + const val KEY_TEXT_REPLY = "key_text_reply" + } +} + +class NotificationBroadcastReceiverHandler @Inject constructor( + private val appCoroutineScope: CoroutineScope, + private val matrixClientProvider: MatrixClientProvider, + private val sessionPreferencesStore: SessionPreferencesStoreFactory, + private val notificationDrawerManager: NotificationDrawerManager, + private val actionIds: NotificationActionIds, + private val systemClock: SystemClock, + private val onNotifiableEventReceived: OnNotifiableEventReceived, +) { + fun onReceive(context: Context, intent: Intent) { + val sessionId = intent.extras?.getString(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return + val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId) + val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId) + val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId) Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") when (intent.action) { @@ -203,17 +218,9 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { if (intent != null) { val remoteInput = RemoteInput.getResultsFromIntent(intent) if (remoteInput != null) { - return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() + return remoteInput.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString() } } return null } - - companion object { - const val KEY_SESSION_ID = "sessionID" - const val KEY_ROOM_ID = "roomID" - const val KEY_THREAD_ID = "threadID" - const val KEY_EVENT_ID = "eventID" - const val KEY_TEXT_REPLY = "key_text_reply" - } } From cface66f205eab8162be6f221f4e1d476a3e7ba3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 09:43:19 +0200 Subject: [PATCH 06/14] Do not provide the context, but use the StringProvider. --- .../notifications/NotificationBroadcastReceiver.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index c78aa55c8b..5996bb79b6 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.push.api.notifications.NotificationDrawerMan import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived +import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first @@ -54,7 +55,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return context.bindings().inject(this) - notificationBroadcastReceiverHandler.onReceive(context, intent) + notificationBroadcastReceiverHandler.onReceive(intent) } companion object { @@ -74,8 +75,9 @@ class NotificationBroadcastReceiverHandler @Inject constructor( private val actionIds: NotificationActionIds, private val systemClock: SystemClock, private val onNotifiableEventReceived: OnNotifiableEventReceived, + private val stringProvider: StringProvider, ) { - fun onReceive(context: Context, intent: Intent) { + fun onReceive(intent: Intent) { val sessionId = intent.extras?.getString(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId) val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId) @@ -84,7 +86,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") when (intent.action) { actionIds.smartReply -> if (roomId != null) { - handleSmartReply(sessionId, roomId, threadId, intent, context) + handleSmartReply(sessionId, roomId, threadId, intent) } actionIds.dismissRoom -> if (roomId != null) { notificationDrawerManager.clearMessagesForRoom(sessionId, roomId) @@ -138,7 +140,6 @@ class NotificationBroadcastReceiverHandler @Inject constructor( roomId: RoomId, threadId: ThreadId?, intent: Intent, - context: Context ) = appCoroutineScope.launch { val message = getReplyMessage(intent) @@ -155,7 +156,6 @@ class NotificationBroadcastReceiverHandler @Inject constructor( threadId = threadId, room = room, message = message, - context = context, ) } } @@ -166,7 +166,6 @@ class NotificationBroadcastReceiverHandler @Inject constructor( threadId: ThreadId?, room: MatrixRoom, message: String, - context: Context, ) { // Create a new event to be displayed in the notification drawer, right now val notifiableMessageEvent = NotifiableMessageEvent( @@ -180,7 +179,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( noisy = false, timestamp = systemClock.epochMillis(), senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId).getOrNull()?.disambiguatedDisplayName - ?: context.getString(R.string.notification_sender_me), + ?: stringProvider.getString(R.string.notification_sender_me), body = message, imageUriString = null, threadId = threadId, From 6eaa6a6b990f686fc7959c85f6fe48626a6b7bfc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 09:44:17 +0200 Subject: [PATCH 07/14] Extract NotificationBroadcastReceiverHandler to its own file --- .../NotificationBroadcastReceiver.kt | 181 ---------------- .../NotificationBroadcastReceiverHandler.kt | 202 ++++++++++++++++++ 2 files changed, 202 insertions(+), 181 deletions(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 5996bb79b6..5933cb885f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -19,33 +19,9 @@ package io.element.android.libraries.push.impl.notifications import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import androidx.core.app.RemoteInput -import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.architecture.bindings -import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.libraries.matrix.api.core.asEventId -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.push.api.notifications.NotificationDrawerManager -import io.element.android.libraries.push.impl.R -import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent -import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived -import io.element.android.services.toolbox.api.strings.StringProvider -import io.element.android.services.toolbox.api.systemclock.SystemClock -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.UUID import javax.inject.Inject -private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag) - /** * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.). */ @@ -66,160 +42,3 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { const val KEY_TEXT_REPLY = "key_text_reply" } } - -class NotificationBroadcastReceiverHandler @Inject constructor( - private val appCoroutineScope: CoroutineScope, - private val matrixClientProvider: MatrixClientProvider, - private val sessionPreferencesStore: SessionPreferencesStoreFactory, - private val notificationDrawerManager: NotificationDrawerManager, - private val actionIds: NotificationActionIds, - private val systemClock: SystemClock, - private val onNotifiableEventReceived: OnNotifiableEventReceived, - private val stringProvider: StringProvider, -) { - fun onReceive(intent: Intent) { - val sessionId = intent.extras?.getString(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return - val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId) - val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId) - val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId) - - Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") - when (intent.action) { - actionIds.smartReply -> if (roomId != null) { - handleSmartReply(sessionId, roomId, threadId, intent) - } - actionIds.dismissRoom -> if (roomId != null) { - notificationDrawerManager.clearMessagesForRoom(sessionId, roomId) - } - actionIds.dismissSummary -> - notificationDrawerManager.clearAllMessagesEvents(sessionId) - actionIds.dismissInvite -> if (roomId != null) { - notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) - } - actionIds.dismissEvent -> if (eventId != null) { - notificationDrawerManager.clearEvent(sessionId, eventId) - } - actionIds.markRoomRead -> if (roomId != null) { - notificationDrawerManager.clearMessagesForRoom(sessionId, roomId) - handleMarkAsRead(sessionId, roomId) - } - actionIds.join -> if (roomId != null) { - notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) - handleJoinRoom(sessionId, roomId) - } - actionIds.reject -> if (roomId != null) { - notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) - handleRejectRoom(sessionId, roomId) - } - } - } - - private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch { - val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch - client.joinRoom(roomId) - } - - private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch { - val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch - client.getRoom(roomId)?.leave() - } - - private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch { - val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch - val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first() - val receiptType = if (isSendPublicReadReceiptsEnabled) { - ReceiptType.READ - } else { - ReceiptType.READ_PRIVATE - } - client.getRoom(roomId)?.markAsRead(receiptType = receiptType) - } - - private fun handleSmartReply( - sessionId: SessionId, - roomId: RoomId, - threadId: ThreadId?, - intent: Intent, - ) = appCoroutineScope.launch { - val message = getReplyMessage(intent) - - if (message.isNullOrBlank()) { - // ignore this event - // Can this happen? should we update notification? - return@launch - } - val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch - client.getRoom(roomId)?.let { room -> - sendMatrixEvent( - sessionId = sessionId, - roomId = roomId, - threadId = threadId, - room = room, - message = message, - ) - } - } - - private suspend fun sendMatrixEvent( - sessionId: SessionId, - roomId: RoomId, - threadId: ThreadId?, - room: MatrixRoom, - message: String, - ) { - // Create a new event to be displayed in the notification drawer, right now - val notifiableMessageEvent = NotifiableMessageEvent( - sessionId = sessionId, - roomId = roomId, - // Generate a Fake event id - eventId = EventId("\$" + UUID.randomUUID().toString()), - editedEventId = null, - canBeReplaced = false, - senderId = sessionId, - noisy = false, - timestamp = systemClock.epochMillis(), - senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId).getOrNull()?.disambiguatedDisplayName - ?: stringProvider.getString(R.string.notification_sender_me), - body = message, - imageUriString = null, - threadId = threadId, - roomName = room.displayName, - roomIsDirect = room.isDirect, - outGoingMessage = true, - ) - onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) - - if (threadId != null) { - room.liveTimeline.replyMessage( - eventId = threadId.asEventId(), - body = message, - htmlBody = null, - mentions = emptyList(), - fromNotification = true, - ) - } else { - room.liveTimeline.sendMessage( - body = message, - htmlBody = null, - mentions = emptyList() - ) - }.onFailure { - Timber.e(it, "Failed to send smart reply message") - onNotifiableEventReceived.onNotifiableEventReceived( - notifiableMessageEvent.copy( - outGoingMessageFailed = true - ) - ) - } - } - - private fun getReplyMessage(intent: Intent?): String? { - if (intent != null) { - val remoteInput = RemoteInput.getResultsFromIntent(intent) - if (remoteInput != null) { - return remoteInput.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString() - } - } - return null - } -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt new file mode 100644 index 0000000000..cfbaea87d3 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Intent +import androidx.core.app.RemoteInput +import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.core.asEventId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.push.api.notifications.NotificationDrawerManager +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag) + +class NotificationBroadcastReceiverHandler @Inject constructor( + private val appCoroutineScope: CoroutineScope, + private val matrixClientProvider: MatrixClientProvider, + private val sessionPreferencesStore: SessionPreferencesStoreFactory, + private val notificationDrawerManager: NotificationDrawerManager, + private val actionIds: NotificationActionIds, + private val systemClock: SystemClock, + private val onNotifiableEventReceived: OnNotifiableEventReceived, + private val stringProvider: StringProvider, +) { + fun onReceive(intent: Intent) { + val sessionId = intent.extras?.getString(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return + val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId) + val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId) + val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId) + + Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}") + when (intent.action) { + actionIds.smartReply -> if (roomId != null) { + handleSmartReply(sessionId, roomId, threadId, intent) + } + actionIds.dismissRoom -> if (roomId != null) { + notificationDrawerManager.clearMessagesForRoom(sessionId, roomId) + } + actionIds.dismissSummary -> + notificationDrawerManager.clearAllMessagesEvents(sessionId) + actionIds.dismissInvite -> if (roomId != null) { + notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + } + actionIds.dismissEvent -> if (eventId != null) { + notificationDrawerManager.clearEvent(sessionId, eventId) + } + actionIds.markRoomRead -> if (roomId != null) { + notificationDrawerManager.clearMessagesForRoom(sessionId, roomId) + handleMarkAsRead(sessionId, roomId) + } + actionIds.join -> if (roomId != null) { + notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + handleJoinRoom(sessionId, roomId) + } + actionIds.reject -> if (roomId != null) { + notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId) + handleRejectRoom(sessionId, roomId) + } + } + } + + private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch { + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch + client.joinRoom(roomId) + } + + private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch { + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch + client.getRoom(roomId)?.leave() + } + + private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch { + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch + val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first() + val receiptType = if (isSendPublicReadReceiptsEnabled) { + ReceiptType.READ + } else { + ReceiptType.READ_PRIVATE + } + client.getRoom(roomId)?.markAsRead(receiptType = receiptType) + } + + private fun handleSmartReply( + sessionId: SessionId, + roomId: RoomId, + threadId: ThreadId?, + intent: Intent, + ) = appCoroutineScope.launch { + val message = getReplyMessage(intent) + + if (message.isNullOrBlank()) { + // ignore this event + // Can this happen? should we update notification? + return@launch + } + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch + client.getRoom(roomId)?.let { room -> + sendMatrixEvent( + sessionId = sessionId, + roomId = roomId, + threadId = threadId, + room = room, + message = message, + ) + } + } + + private suspend fun sendMatrixEvent( + sessionId: SessionId, + roomId: RoomId, + threadId: ThreadId?, + room: MatrixRoom, + message: String, + ) { + // Create a new event to be displayed in the notification drawer, right now + val notifiableMessageEvent = NotifiableMessageEvent( + sessionId = sessionId, + roomId = roomId, + // Generate a Fake event id + eventId = EventId("\$" + UUID.randomUUID().toString()), + editedEventId = null, + canBeReplaced = false, + senderId = sessionId, + noisy = false, + timestamp = systemClock.epochMillis(), + senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId) + .getOrNull()?.disambiguatedDisplayName + ?: stringProvider.getString(R.string.notification_sender_me), + body = message, + imageUriString = null, + threadId = threadId, + roomName = room.displayName, + roomIsDirect = room.isDirect, + outGoingMessage = true, + ) + onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) + + if (threadId != null) { + room.liveTimeline.replyMessage( + eventId = threadId.asEventId(), + body = message, + htmlBody = null, + mentions = emptyList(), + fromNotification = true, + ) + } else { + room.liveTimeline.sendMessage( + body = message, + htmlBody = null, + mentions = emptyList() + ) + }.onFailure { + Timber.e(it, "Failed to send smart reply message") + onNotifiableEventReceived.onNotifiableEventReceived( + notifiableMessageEvent.copy( + outGoingMessageFailed = true + ) + ) + } + } + + private fun getReplyMessage(intent: Intent?): String? { + if (intent != null) { + val remoteInput = RemoteInput.getResultsFromIntent(intent) + if (remoteInput != null) { + return remoteInput.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString() + } + } + return null + } +} From d8c68e5b195afcb3595cee52177564c2ef00b0d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 10:05:18 +0200 Subject: [PATCH 08/14] Remove unused actions and fix comment --- .../push/impl/notifications/NotificationActionIds.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt index 6141079130..1907d11732 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt @@ -20,16 +20,13 @@ import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject /** - * Util class for creating notifications. - * Note: Cannot inject ColorProvider in the constructor, because it requires an Activity + * Util class for creating notifications action Ids, using the application id. */ - data class NotificationActionIds @Inject constructor( private val buildMeta: BuildMeta, ) { val join = "${buildMeta.applicationId}.NotificationActions.JOIN_ACTION" val reject = "${buildMeta.applicationId}.NotificationActions.REJECT_ACTION" - val quickLaunch = "${buildMeta.applicationId}.NotificationActions.QUICK_LAUNCH_ACTION" val markRoomRead = "${buildMeta.applicationId}.NotificationActions.MARK_ROOM_READ_ACTION" val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION" val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION" @@ -37,5 +34,4 @@ data class NotificationActionIds @Inject constructor( val dismissInvite = "${buildMeta.applicationId}.NotificationActions.DISMISS_INVITE_NOTIF_ACTION" val dismissEvent = "${buildMeta.applicationId}.NotificationActions.DISMISS_EVENT_NOTIF_ACTION" val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC" - val push = "${buildMeta.applicationId}.PUSH" } From 4db9b5ee1169a576aeab97e83a1c846cf5199606 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 10:18:07 +0200 Subject: [PATCH 09/14] Use getStringExtra --- .../impl/notifications/NotificationBroadcastReceiverHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index cfbaea87d3..b7676db308 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -54,7 +54,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( private val stringProvider: StringProvider, ) { fun onReceive(intent: Intent) { - val sessionId = intent.extras?.getString(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return + val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId) val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId) val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId) From 2f2605dc766abcf76ae2f74e9364665e89ce308d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 12:05:16 +0200 Subject: [PATCH 10/14] Use lambdaError() and val instead of var. --- .../preferences/test/FakeSessionPreferencesStoreFactory.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/FakeSessionPreferencesStoreFactory.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/FakeSessionPreferencesStoreFactory.kt index 96761a9712..aee59c942b 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/FakeSessionPreferencesStoreFactory.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/FakeSessionPreferencesStoreFactory.kt @@ -21,12 +21,13 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.CoroutineScope class FakeSessionPreferencesStoreFactory( - var getLambda: LambdaTwoParamsRecorder = lambdaRecorder { _, _ -> throw NotImplementedError() }, - var removeLambda: LambdaOneParamRecorder = lambdaRecorder { _ -> }, + val getLambda: LambdaTwoParamsRecorder = lambdaRecorder { _, _ -> lambdaError() }, + val removeLambda: LambdaOneParamRecorder = lambdaRecorder { _ -> lambdaError() }, ) : SessionPreferencesStoreFactory { override fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore { return getLambda(sessionId, sessionCoroutineScope) From 5b69a0320827b8ffa05cb799604a45e2f2f94e27 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 12:31:21 +0200 Subject: [PATCH 11/14] Fix existing tests. --- .../AcceptDeclineInvitePresenterTest.kt | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt index 72552b9ed1..672c5b58ee 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt @@ -23,8 +23,10 @@ import io.element.android.features.invite.api.response.InviteData import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom @@ -128,6 +130,12 @@ class AcceptDeclineInvitePresenterTest { @Test fun `present - declining invite success flow`() = runTest { + val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> + Result.success(Unit) + } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda + ) val declineInviteSuccess = lambdaRecorder { -> Result.success(Unit) } @@ -139,7 +147,10 @@ class AcceptDeclineInvitePresenterTest { } ) } - val presenter = createAcceptDeclineInvitePresenter(client = client) + val presenter = createAcceptDeclineInvitePresenter( + client = client, + notificationDrawerManager = notificationDrawerManager, + ) presenter.test { val inviteData = anInviteData() awaitItem().also { state -> @@ -159,7 +170,10 @@ class AcceptDeclineInvitePresenterTest { } cancelAndConsumeRemainingEvents() } - assert(declineInviteSuccess).isCalledOnce() + declineInviteSuccess.assertions().isCalledOnce() + clearMembershipNotificationForRoomLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID)) } @Test @@ -202,10 +216,19 @@ class AcceptDeclineInvitePresenterTest { @Test fun `present - accepting invite success flow`() = runTest { + val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> + Result.success(Unit) + } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda + ) val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List, _: JoinedRoom.Trigger -> Result.success(Unit) } - val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomSuccess) + val presenter = createAcceptDeclineInvitePresenter( + joinRoomLambda = joinRoomSuccess, + notificationDrawerManager = notificationDrawerManager, + ) presenter.test { val inviteData = anInviteData() awaitItem().also { state -> @@ -229,6 +252,9 @@ class AcceptDeclineInvitePresenterTest { value(emptyList()), value(JoinedRoom.Trigger.Invite) ) + clearMembershipNotificationForRoomLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID)) } private fun anInviteData( From add5c39db0f8e751778bd3bbc8c0d55da4194c82 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 12:34:22 +0200 Subject: [PATCH 12/14] Fix existing tests. --- .../features/migration/impl/migrations/AppMigration02Test.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt index 6bb2f5babd..3df93806d5 100644 --- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt @@ -35,6 +35,7 @@ class AppMigration02Test { val sessionPreferencesStore = InMemorySessionPreferencesStore(isSessionVerificationSkipped = false) val sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory( getLambda = lambdaRecorder { _, _, -> sessionPreferencesStore }, + removeLambda = lambdaRecorder { _ -> } ) val migration = AppMigration02(sessionStore = sessionStore, sessionPreferenceStoreFactory = sessionPreferencesStoreFactory) From afe5c33f3323139aaa2ba28af82ee032d002dd86 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 14:50:22 +0200 Subject: [PATCH 13/14] Add test on NotificationBroadcastReceiverHandler --- libraries/push/impl/build.gradle.kts | 1 + .../NotificationBroadcastReceiverHandler.kt | 15 +- .../notifications/ReplyMessageExtractor.kt | 35 ++ .../FakeReplyMessageExtractor.kt | 27 + ...otificationBroadcastReceiverHandlerTest.kt | 474 ++++++++++++++++++ .../push/FakeOnNotifiableEventReceived.kt | 3 +- .../FakeNotificationDrawerManager.kt | 32 +- 7 files changed, 554 insertions(+), 33 deletions(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeReplyMessageExtractor.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 36dddbcedf..85b1d80942 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { testImplementation(libs.coil.test) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushstore.test) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index b7676db308..530ab54e8f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -17,7 +17,6 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent -import androidx.core.app.RemoteInput import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.MatrixClientProvider @@ -52,6 +51,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( private val systemClock: SystemClock, private val onNotifiableEventReceived: OnNotifiableEventReceived, private val stringProvider: StringProvider, + private val replyMessageExtractor: ReplyMessageExtractor, ) { fun onReceive(intent: Intent) { val sessionId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return @@ -117,8 +117,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor( threadId: ThreadId?, intent: Intent, ) = appCoroutineScope.launch { - val message = getReplyMessage(intent) - + val message = replyMessageExtractor.getReplyMessage(intent) if (message.isNullOrBlank()) { // ignore this event // Can this happen? should we update notification? @@ -189,14 +188,4 @@ class NotificationBroadcastReceiverHandler @Inject constructor( ) } } - - private fun getReplyMessage(intent: Intent?): String? { - if (intent != null) { - val remoteInput = RemoteInput.getResultsFromIntent(intent) - if (remoteInput != null) { - return remoteInput.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString() - } - } - return null - } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt new file mode 100644 index 0000000000..e33f144d6f --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Intent +import androidx.core.app.RemoteInput +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +interface ReplyMessageExtractor { + fun getReplyMessage(intent: Intent): String? +} + +@ContributesBinding(AppScope::class) +class AndroidReplyMessageExtractor @Inject constructor() : ReplyMessageExtractor { + override fun getReplyMessage(intent: Intent): String? { + return RemoteInput.getResultsFromIntent(intent) + ?.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeReplyMessageExtractor.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeReplyMessageExtractor.kt new file mode 100644 index 0000000000..4baeedc92b --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeReplyMessageExtractor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Intent + +class FakeReplyMessageExtractor( + private val result: String? = null, +) : ReplyMessageExtractor { + override fun getReplyMessage(intent: Intent): String? { + return result + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt new file mode 100644 index 0000000000..7266f9fe98 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt @@ -0,0 +1,474 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Intent +import com.google.common.truth.Truth.assertThat +import io.element.android.features.preferences.api.store.SessionPreferencesStore +import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.core.asEventId +import io.element.android.libraries.matrix.api.room.Mention +import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory +import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore +import io.element.android.libraries.push.api.notifications.NotificationDrawerManager +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived +import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived +import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.api.systemclock.SystemClock +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +class NotificationBroadcastReceiverHandlerTest { + private val actionIds = NotificationActionIds(aBuildMeta()) + + @Test + fun `When no sessionId, nothing happen`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.join, + sessionId = null + ), + ) + } + + @Test + fun `Test dismiss room without a roomId, nothing happen`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.dismissRoom, + ), + ) + } + + @Test + fun `Test dismiss room`() = runTest { + val clearMessagesForRoomLambda = lambdaRecorder { _, _ -> } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearMessagesForRoomLambda = clearMessagesForRoomLambda, + ) + val sut = createNotificationBroadcastReceiverHandler( + notificationDrawerManager = notificationDrawerManager + ) + sut.onReceive( + createIntent( + action = actionIds.dismissRoom, + roomId = A_ROOM_ID, + ), + ) + runCurrent() + clearMessagesForRoomLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID)) + } + + @Test + fun `Test dismiss summary`() = runTest { + val clearAllMessagesEventsLambda = lambdaRecorder { _ -> } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearAllMessagesEventsLambda = clearAllMessagesEventsLambda, + ) + val sut = createNotificationBroadcastReceiverHandler( + notificationDrawerManager = notificationDrawerManager + ) + sut.onReceive( + createIntent( + action = actionIds.dismissSummary, + ), + ) + clearAllMessagesEventsLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID)) + } + + @Test + fun `Test dismiss Invite without room`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.dismissInvite, + ), + ) + } + + @Test + fun `Test dismiss Invite`() = runTest { + val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda, + ) + val sut = createNotificationBroadcastReceiverHandler( + notificationDrawerManager = notificationDrawerManager + ) + sut.onReceive( + createIntent( + action = actionIds.dismissInvite, + roomId = A_ROOM_ID, + ), + ) + clearMembershipNotificationForRoomLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID)) + } + + @Test + fun `Test dismiss Event without event`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.dismissEvent, + ), + ) + } + + @Test + fun `Test dismiss Event`() = runTest { + val clearEventLambda = lambdaRecorder { _, _ -> } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearEventLambda = clearEventLambda, + ) + val sut = createNotificationBroadcastReceiverHandler( + notificationDrawerManager = notificationDrawerManager + ) + sut.onReceive( + createIntent( + action = actionIds.dismissEvent, + eventId = AN_EVENT_ID, + ), + ) + clearEventLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(AN_EVENT_ID)) + } + + @Test + fun `Test mark room as read without room`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.markRoomRead, + ), + ) + } + + @Test + fun `Test mark room as read, send public RR`() { + testMarkRoomAsRead( + isSendPublicReadReceiptsEnabled = true, + expectedReceiptType = ReceiptType.READ + ) + } + + @Test + fun `Test mark room as read, send private RR`() { + testMarkRoomAsRead( + isSendPublicReadReceiptsEnabled = false, + expectedReceiptType = ReceiptType.READ_PRIVATE + ) + } + + private fun testMarkRoomAsRead( + isSendPublicReadReceiptsEnabled: Boolean, + expectedReceiptType: ReceiptType, + ) = runTest { + val getLambda = lambdaRecorder { _, _ -> + InMemorySessionPreferencesStore( + isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled + ) + } + val sessionPreferencesStore = FakeSessionPreferencesStoreFactory( + getLambda = getLambda + ) + val clearMessagesForRoomLambda = lambdaRecorder { _, _ -> } + val matrixRoom = FakeMatrixRoom() + val notificationDrawerManager = FakeNotificationDrawerManager( + clearMessagesForRoomLambda = clearMessagesForRoomLambda, + ) + val sut = createNotificationBroadcastReceiverHandler( + sessionPreferencesStore = sessionPreferencesStore, + matrixRoom = matrixRoom, + notificationDrawerManager = notificationDrawerManager + ) + sut.onReceive( + createIntent( + action = actionIds.markRoomRead, + roomId = A_ROOM_ID, + ), + ) + runCurrent() + clearMessagesForRoomLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID)) + assertThat(matrixRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType)) + } + + @Test + fun `Test join room without room`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.join, + ), + ) + } + + @Test + fun `Test join room`() = runTest { + val joinRoom = lambdaRecorder> { _ -> Result.success(Unit) } + val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda, + ) + val sut = createNotificationBroadcastReceiverHandler( + joinRoom = joinRoom, + notificationDrawerManager = notificationDrawerManager, + ) + sut.onReceive( + createIntent( + action = actionIds.join, + roomId = A_ROOM_ID, + ), + ) + runCurrent() + joinRoom.assertions() + .isCalledOnce() + .with(value(A_ROOM_ID)) + clearMembershipNotificationForRoomLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID)) + } + + @Test + fun `Test reject room without room`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.reject, + ), + ) + } + + @Test + fun `Test reject room`() = runTest { + val leaveRoom = lambdaRecorder> { Result.success(Unit) } + val matrixRoom = FakeMatrixRoom().apply { + leaveRoomLambda = leaveRoom + } + val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> } + val notificationDrawerManager = FakeNotificationDrawerManager( + clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda, + ) + val sut = createNotificationBroadcastReceiverHandler( + matrixRoom = matrixRoom, + notificationDrawerManager = notificationDrawerManager + ) + sut.onReceive( + createIntent( + action = actionIds.reject, + roomId = A_ROOM_ID, + ), + ) + runCurrent() + clearMembershipNotificationForRoomLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value(A_ROOM_ID)) + leaveRoom.assertions() + .isCalledOnce() + .with() + } + + @Test + fun `Test send reply without room`() = runTest { + val sut = createNotificationBroadcastReceiverHandler() + sut.onReceive( + createIntent( + action = actionIds.smartReply, + ), + ) + } + + @Test + fun `Test send reply`() = runTest { + val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } + val replyMessage = lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + val liveTimeline = FakeTimeline().apply { + sendMessageLambda = sendMessage + replyMessageLambda = replyMessage + } + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val onNotifiableEventReceivedResult = lambdaRecorder { _ -> } + val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) + val sut = createNotificationBroadcastReceiverHandler( + matrixRoom = matrixRoom, + onNotifiableEventReceived = onNotifiableEventReceived, + replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE) + ) + sut.onReceive( + createIntent( + action = actionIds.smartReply, + roomId = A_ROOM_ID, + ), + ) + runCurrent() + sendMessage.assertions() + .isCalledOnce() + .with(value(A_MESSAGE), value(null), value(emptyList())) + onNotifiableEventReceivedResult.assertions() + .isCalledOnce() + replyMessage.assertions() + .isNeverCalled() + } + + @Test + fun `Test send reply blank message`() = runTest { + val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } + val liveTimeline = FakeTimeline().apply { + sendMessageLambda = sendMessage + } + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val sut = createNotificationBroadcastReceiverHandler( + matrixRoom = matrixRoom, + replyMessageExtractor = FakeReplyMessageExtractor(" "), + ) + sut.onReceive( + createIntent( + action = actionIds.smartReply, + roomId = A_ROOM_ID, + ), + ) + runCurrent() + sendMessage.assertions() + .isNeverCalled() + } + + @Test + fun `Test send reply to thread`() = runTest { + val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } + val replyMessage = lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + val liveTimeline = FakeTimeline().apply { + sendMessageLambda = sendMessage + replyMessageLambda = replyMessage + } + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val onNotifiableEventReceivedResult = lambdaRecorder { _ -> } + val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) + val sut = createNotificationBroadcastReceiverHandler( + matrixRoom = matrixRoom, + onNotifiableEventReceived = onNotifiableEventReceived, + replyMessageExtractor = FakeReplyMessageExtractor(A_MESSAGE) + ) + sut.onReceive( + createIntent( + action = actionIds.smartReply, + roomId = A_ROOM_ID, + threadId = A_THREAD_ID, + ), + ) + runCurrent() + sendMessage.assertions() + .isNeverCalled() + onNotifiableEventReceivedResult.assertions() + .isCalledOnce() + replyMessage.assertions() + .isCalledOnce() + .with(value(A_THREAD_ID.asEventId()), value(A_MESSAGE), value(null), value(emptyList()), value(true)) + } + + private fun createIntent( + action: String, + sessionId: SessionId? = A_SESSION_ID, + roomId: RoomId? = null, + eventId: EventId? = null, + threadId: ThreadId? = null, + ) = Intent(action).apply { + putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId?.value) + putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId?.value) + putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, threadId?.value) + putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, eventId?.value) + } + + private fun TestScope.createNotificationBroadcastReceiverHandler( + matrixRoom: FakeMatrixRoom? = FakeMatrixRoom(), + joinRoom: (RoomId) -> Result = { lambdaError() }, + matrixClient: MatrixClient? = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, matrixRoom) + joinRoomLambda = joinRoom + }, + sessionPreferencesStore: SessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(), + notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(), + systemClock: SystemClock = FakeSystemClock(), + onNotifiableEventReceived: OnNotifiableEventReceived = FakeOnNotifiableEventReceived(), + stringProvider: StringProvider = FakeStringProvider(), + replyMessageExtractor: ReplyMessageExtractor = FakeReplyMessageExtractor(), + ): NotificationBroadcastReceiverHandler { + return NotificationBroadcastReceiverHandler( + appCoroutineScope = this, + matrixClientProvider = FakeMatrixClientProvider { + if (matrixClient == null) { + Result.failure(Exception("No matrix client")) + } else { + Result.success(matrixClient) + } + }, + sessionPreferencesStore = sessionPreferencesStore, + notificationDrawerManager = notificationDrawerManager, + actionIds = actionIds, + systemClock = systemClock, + onNotifiableEventReceived = onNotifiableEventReceived, + stringProvider = stringProvider, + replyMessageExtractor = replyMessageExtractor, + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt index 3c9e025830..d7fb3ce048 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt @@ -17,9 +17,10 @@ package io.element.android.libraries.push.impl.push import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.tests.testutils.lambda.lambdaError class FakeOnNotifiableEventReceived( - private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit, + private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit = { lambdaError() }, ) : OnNotifiableEventReceived { override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { onNotifiableEventReceivedResult(notifiableEvent) diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt index 9b932cd79a..f0cbcd1557 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt @@ -20,39 +20,33 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.notifications.NotificationDrawerManager +import io.element.android.tests.testutils.lambda.lambdaError -class FakeNotificationDrawerManager : NotificationDrawerManager { - private val clearMemberShipNotificationForSessionCallsCount = mutableMapOf() - private val clearMemberShipNotificationForRoomCallsCount = mutableMapOf() +class FakeNotificationDrawerManager( + private val clearAllMessagesEventsLambda: (SessionId) -> Unit = { lambdaError() }, + private val clearMessagesForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() }, + private val clearEventLambda: (SessionId, EventId) -> Unit = { _, _ -> lambdaError() }, + private val clearMembershipNotificationForSessionLambda: (SessionId) -> Unit = { lambdaError() }, + private val clearMembershipNotificationForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() } +) : NotificationDrawerManager { override fun clearAllMessagesEvents(sessionId: SessionId) { + clearAllMessagesEventsLambda(sessionId) } override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { + clearMessagesForRoomLambda(sessionId, roomId) } override fun clearEvent(sessionId: SessionId, eventId: EventId) { + clearEventLambda(sessionId, eventId) } override fun clearMembershipNotificationForSession(sessionId: SessionId) { - clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value } + clearMembershipNotificationForSessionLambda(sessionId) } override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) { - val key = getMembershipNotificationKey(sessionId, roomId) - clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value } - } - - fun getClearMembershipNotificationForSessionCount(sessionId: SessionId): Int { - return clearMemberShipNotificationForRoomCallsCount[sessionId.value] ?: 0 - } - - fun getClearMembershipNotificationForRoomCount(sessionId: SessionId, roomId: RoomId): Int { - val key = getMembershipNotificationKey(sessionId, roomId) - return clearMemberShipNotificationForRoomCallsCount[key] ?: 0 - } - - private fun getMembershipNotificationKey(sessionId: SessionId, roomId: RoomId): String { - return "$sessionId-$roomId" + clearMembershipNotificationForRoomLambda(sessionId, roomId) } } From acefbbc7d8578c4379e976a7a2eaa92b7881486c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Jun 2024 15:11:51 +0200 Subject: [PATCH 14/14] Fix formatting issues. --- .../notifications/NotificationBroadcastReceiverHandler.kt | 4 ++-- .../push/impl/notifications/ReplyMessageExtractor.kt | 3 ++- .../push/test/notifications/FakeNotificationDrawerManager.kt | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index 530ab54e8f..1dc529cf58 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -153,8 +153,8 @@ class NotificationBroadcastReceiverHandler @Inject constructor( senderId = sessionId, noisy = false, timestamp = systemClock.epochMillis(), - senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId) - .getOrNull()?.disambiguatedDisplayName + senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId).getOrNull() + ?.disambiguatedDisplayName ?: stringProvider.getString(R.string.notification_sender_me), body = message, imageUriString = null, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt index e33f144d6f..8c7fc5dbd8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt @@ -30,6 +30,7 @@ interface ReplyMessageExtractor { class AndroidReplyMessageExtractor @Inject constructor() : ReplyMessageExtractor { override fun getReplyMessage(intent: Intent): String? { return RemoteInput.getResultsFromIntent(intent) - ?.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString() + ?.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY) + ?.toString() } } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt index f0cbcd1557..b42f05aa5f 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt @@ -29,7 +29,6 @@ class FakeNotificationDrawerManager( private val clearMembershipNotificationForSessionLambda: (SessionId) -> Unit = { lambdaError() }, private val clearMembershipNotificationForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() } ) : NotificationDrawerManager { - override fun clearAllMessagesEvents(sessionId: SessionId) { clearAllMessagesEventsLambda(sessionId) }