Browse Source

Merge pull request #2967 from element-hq/feature/bma/notificationActions

Quick reply in notification actions
pull/2985/head
Benoit Marty 4 months ago committed by GitHub
parent
commit
459b4ee9bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 32
      features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt
  2. 8
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  3. 1
      features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt
  4. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt
  5. 8
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt
  6. 25
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
  7. 7
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt
  8. 5
      libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/FakeSessionPreferencesStoreFactory.kt
  9. 5
      libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt
  10. 1
      libraries/push/impl/build.gradle.kts
  11. 6
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
  12. 6
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
  13. 194
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt
  14. 191
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt
  15. 36
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt
  16. 27
      libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeReplyMessageExtractor.kt
  17. 474
      libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt
  18. 3
      libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt
  19. 37
      libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt

32
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 @@ -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 { @@ -128,6 +130,12 @@ class AcceptDeclineInvitePresenterTest {
@Test
fun `present - declining invite success flow`() = runTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val notificationDrawerManager = FakeNotificationDrawerManager(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val declineInviteSuccess = lambdaRecorder { ->
Result.success(Unit)
}
@ -139,7 +147,10 @@ class AcceptDeclineInvitePresenterTest { @@ -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 { @@ -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 { @@ -202,10 +216,19 @@ class AcceptDeclineInvitePresenterTest {
@Test
fun `present - accepting invite success flow`() = runTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val notificationDrawerManager = FakeNotificationDrawerManager(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: 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 { @@ -229,6 +252,9 @@ class AcceptDeclineInvitePresenterTest {
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
}
private fun anInviteData(

8
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt

@ -395,7 +395,7 @@ class MessageComposerPresenterTest { @@ -395,7 +395,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - reply message`() = runTest {
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention> ->
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
@ -426,7 +426,7 @@ class MessageComposerPresenterTest { @@ -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(
@ -909,7 +909,7 @@ class MessageComposerPresenterTest { @@ -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<Mention> ->
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
Result.success(Unit)
}
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
@ -956,7 +956,7 @@ class MessageComposerPresenterTest { @@ -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)

1
features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration02Test.kt

@ -35,6 +35,7 @@ class AppMigration02Test { @@ -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)

2
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 { @@ -33,3 +33,5 @@ value class ThreadId(val value: String) : Serializable {
override fun toString(): String = value
}
fun ThreadId.asEventId(): EventId = EventId(value)

8
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt

@ -57,7 +57,13 @@ interface Timeline : AutoCloseable { @@ -57,7 +57,13 @@ interface Timeline : AutoCloseable {
suspend fun enterSpecialMode(eventId: EventId?): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
suspend fun replyMessage(
eventId: EventId,
body: String,
htmlBody: String?,
mentions: List<Mention>,
fromNotification: Boolean = false,
): Result<Unit>
suspend fun sendImage(
file: File,

25
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt

@ -308,13 +308,28 @@ class RustTimeline( @@ -308,13 +308,28 @@ class RustTimeline(
}
}
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(dispatcher) {
override suspend fun replyMessage(
eventId: EventId,
body: String,
htmlBody: String?,
mentions: List<Mention>,
fromNotification: Boolean,
): Result<Unit> = 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
}
}

7
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt

@ -104,7 +104,8 @@ class FakeTimeline( @@ -104,7 +104,8 @@ class FakeTimeline(
body: String,
htmlBody: String?,
mentions: List<Mention>,
) -> Result<Unit> = { _, _, _, _ ->
fromNotification: Boolean,
) -> Result<Unit> = { _, _, _, _, _ ->
Result.success(Unit)
}
@ -113,11 +114,13 @@ class FakeTimeline( @@ -113,11 +114,13 @@ class FakeTimeline(
body: String,
htmlBody: String?,
mentions: List<Mention>,
fromNotification: Boolean,
): Result<Unit> = replyMessageLambda(
eventId,
body,
htmlBody,
mentions
mentions,
fromNotification,
)
var sendImageLambda: (

5
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 @@ -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<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> throw NotImplementedError() },
var removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> },
val getLambda: LambdaTwoParamsRecorder<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> lambdaError() },
val removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> lambdaError() },
) : SessionPreferencesStoreFactory {
override fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore {
return getLambda(sessionId, sessionCoroutineScope)

5
libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt

@ -16,10 +16,15 @@ @@ -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)
}

1
libraries/push/impl/build.gradle.kts

@ -71,6 +71,7 @@ dependencies { @@ -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)

6
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt

@ -123,7 +123,7 @@ class DefaultNotificationDrawerManager @Inject constructor( @@ -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( @@ -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( @@ -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)

6
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 @@ -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( @@ -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"
}

194
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt

@ -19,210 +19,20 @@ package io.element.android.libraries.push.impl.notifications @@ -19,210 +19,20 @@ package io.element.android.libraries.push.impl.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
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.timeline.ReceiptType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag)
/**
* 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 defaultNotificationDrawerManager: DefaultNotificationDrawerManager
@Inject lateinit var actionIds: NotificationActionIds
@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 eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId)
context.bindings<NotificationBroadcastReceiverBindings>().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.dismissRoom -> if (roomId != null) {
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
}
actionIds.dismissSummary ->
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId)
actionIds.dismissInvite -> if (roomId != null) {
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
}
actionIds.dismissEvent -> if (eventId != null) {
defaultNotificationDrawerManager.clearEvent(sessionId, eventId)
}
actionIds.markRoomRead -> if (roomId != null) {
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
handleMarkAsRead(sessionId, roomId)
}
actionIds.join -> if (roomId != null) {
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
handleJoinRoom(sessionId, roomId)
}
actionIds.reject -> if (roomId != null) {
defaultNotificationDrawerManager.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)
}
@Suppress("UNUSED_PARAMETER")
private fun handleSmartReply(intent: Intent, context: Context) {
/*
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) {
// ignore this event
// Can this happen? should we update notification?
return
}
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,
)
} else {
room.sendService().sendTextMessage(message)
}
// 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
)
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<Void?> {
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)
}
})
*/
}
private fun getReplyMessage(intent: Intent?): String? {
if (intent != null) {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
if (remoteInput != null) {
return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString()
}
}
return null
notificationBroadcastReceiverHandler.onReceive(intent)
}
*/
companion object {
const val KEY_SESSION_ID = "sessionID"

191
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt

@ -0,0 +1,191 @@ @@ -0,0 +1,191 @@
/*
* 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 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,
private val replyMessageExtractor: ReplyMessageExtractor,
) {
fun onReceive(intent: Intent) {
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)
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 = replyMessageExtractor.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
)
)
}
}
}

36
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ReplyMessageExtractor.kt

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
/*
* 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()
}
}

27
libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeReplyMessageExtractor.kt

@ -0,0 +1,27 @@ @@ -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
}
}

474
libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt

@ -0,0 +1,474 @@ @@ -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<SessionId, RoomId, Unit> { _, _ -> }
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<SessionId, Unit> { _ -> }
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<SessionId, RoomId, Unit> { _, _ -> }
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<SessionId, EventId, Unit> { _, _ -> }
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<SessionId, CoroutineScope, SessionPreferencesStore> { _, _ ->
InMemorySessionPreferencesStore(
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled
)
}
val sessionPreferencesStore = FakeSessionPreferencesStoreFactory(
getLambda = getLambda
)
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
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<RoomId, Result<Unit>> { _ -> Result.success(Unit) }
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
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<Unit>> { Result.success(Unit) }
val matrixRoom = FakeMatrixRoom().apply {
leaveRoomLambda = leaveRoom
}
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
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<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
val replyMessage = lambdaRecorder<EventId, String, String?, List<Mention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
val liveTimeline = FakeTimeline().apply {
sendMessageLambda = sendMessage
replyMessageLambda = replyMessage
}
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline
)
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
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<Mention>()))
onNotifiableEventReceivedResult.assertions()
.isCalledOnce()
replyMessage.assertions()
.isNeverCalled()
}
@Test
fun `Test send reply blank message`() = runTest {
val sendMessage = lambdaRecorder<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> 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<String, String?, List<Mention>, Result<Unit>> { _, _, _ -> Result.success(Unit) }
val replyMessage = lambdaRecorder<EventId, String, String?, List<Mention>, Boolean, Result<Unit>> { _, _, _, _, _ -> Result.success(Unit) }
val liveTimeline = FakeTimeline().apply {
sendMessageLambda = sendMessage
replyMessageLambda = replyMessage
}
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline
)
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> }
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<Mention>()), 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<Unit> = { 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,
)
}
}

3
libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt

@ -17,9 +17,10 @@ @@ -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)

37
libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt

@ -16,33 +16,36 @@ @@ -16,33 +16,36 @@
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
class FakeNotificationDrawerManager : NotificationDrawerManager {
private val clearMemberShipNotificationForSessionCallsCount = mutableMapOf<String, Int>()
private val clearMemberShipNotificationForRoomCallsCount = mutableMapOf<String, Int>()
override fun clearMembershipNotificationForSession(sessionId: SessionId) {
clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value }
import io.element.android.tests.testutils.lambda.lambdaError
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 clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
val key = getMembershipNotificationKey(sessionId, roomId)
clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value }
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
clearMessagesForRoomLambda(sessionId, roomId)
}
fun getClearMembershipNotificationForSessionCount(sessionId: SessionId): Int {
return clearMemberShipNotificationForRoomCallsCount[sessionId.value] ?: 0
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
clearEventLambda(sessionId, eventId)
}
fun getClearMembershipNotificationForRoomCount(sessionId: SessionId, roomId: RoomId): Int {
val key = getMembershipNotificationKey(sessionId, roomId)
return clearMemberShipNotificationForRoomCallsCount[key] ?: 0
override fun clearMembershipNotificationForSession(sessionId: SessionId) {
clearMembershipNotificationForSessionLambda(sessionId)
}
private fun getMembershipNotificationKey(sessionId: SessionId, roomId: RoomId): String {
return "$sessionId-$roomId"
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
clearMembershipNotificationForRoomLambda(sessionId, roomId)
}
}

Loading…
Cancel
Save