Merge pull request #6190 from element-hq/feature/bma/fallbackNotificationCleanup

Fallback notification cleanup
This commit is contained in:
Benoit Marty 2026-02-12 19:00:39 +01:00 committed by GitHub
commit f9210a3506
10 changed files with 375 additions and 73 deletions

View file

@ -1238,7 +1238,7 @@ class MessagesPresenterTest {
} }
@Test @Test
fun `present - shows a "world_readable" icon if the room is encrypted and history is world_readable`() = runTest { fun `present - shows a 'world_readable' icon if the room is encrypted and history is world_readable`() = runTest {
val presenter = createMessagesPresenter( val presenter = createMessagesPresenter(
joinedRoom = FakeJoinedRoom( joinedRoom = FakeJoinedRoom(
baseRoom = FakeBaseRoom( baseRoom = FakeBaseRoom(

View file

@ -22,12 +22,20 @@ import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.NavigationState
import io.element.android.services.appnavstate.api.currentRoomId
import io.element.android.services.appnavstate.api.currentSessionId
import io.element.android.services.appnavstate.api.currentThreadId
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -71,7 +79,10 @@ class DefaultNotificationDrawerManager(
private fun onAppNavigationStateChange(navigationState: NavigationState) { private fun onAppNavigationStateChange(navigationState: NavigationState) {
when (navigationState) { when (navigationState) {
NavigationState.Root -> {} NavigationState.Root -> {}
is NavigationState.Session -> {} is NavigationState.Session -> {
// Cleanup the fallback notification
clearFallbackForSession(navigationState.sessionId)
}
is NavigationState.Room -> { is NavigationState.Room -> {
// Cleanup notification for current room // Cleanup notification for current room
clearMessagesForRoom( clearMessagesForRoom(
@ -94,14 +105,11 @@ class DefaultNotificationDrawerManager(
* Events might be grouped and there might not be one notification per event! * Events might be grouped and there might not be one notification per event!
*/ */
suspend fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { suspend fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
if (notifiableEvent.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value)) { onNotifiableEventsReceived(listOf(notifiableEvent))
return
}
renderEvents(listOf(notifiableEvent))
} }
suspend fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) { suspend fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
val eventsToNotify = notifiableEvents.filter { !it.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value) } val eventsToNotify = notifiableEvents.filter { !appNavigationStateService.appNavigationState.value.shouldIgnoreEvent(it) }
renderEvents(eventsToNotify) renderEvents(eventsToNotify)
} }
@ -121,6 +129,17 @@ class DefaultNotificationDrawerManager(
.forEach { notificationDisplayer.cancelNotification(it.tag, it.id) } .forEach { notificationDisplayer.cancelNotification(it.tag, it.id) }
} }
/**
* Remove the fallback notification for the session.
*/
fun clearFallbackForSession(sessionId: SessionId) {
notificationDisplayer.cancelNotification(
DefaultNotificationDataFactory.FALLBACK_NOTIFICATION_TAG,
NotificationIdProvider.getFallbackNotificationId(sessionId),
)
clearSummaryNotificationIfNeeded(sessionId)
}
/** /**
* Should be called when the application is currently opened and showing timeline for the given [roomId]. * Should be called when the application is currently opened and showing timeline for the given [roomId].
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room. * Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
@ -192,3 +211,30 @@ class DefaultNotificationDrawerManager(
} }
} }
} }
/**
* Used to check if a notifiableEvent should be ignored based on the current application navigation state.
*/
private fun AppNavigationState.shouldIgnoreEvent(event: NotifiableEvent): Boolean {
if (!isInForeground) return false
return navigationState.currentSessionId() == event.sessionId &&
when (event) {
is NotifiableRingingCallEvent -> {
// Never ignore ringing call notifications
// Note that NotifiableRingingCallEvent are not handled by DefaultNotificationDrawerManager
false
}
is FallbackNotifiableEvent -> {
// Ignore if the room list is currently displayed
navigationState is NavigationState.Session
}
is InviteNotifiableEvent,
is SimpleNotifiableEvent -> {
event.roomId == navigationState.currentRoomId()
}
is NotifiableMessageEvent -> {
event.roomId == navigationState.currentRoomId() &&
event.threadId == navigationState.currentThreadId()
}
}
}

View file

@ -144,12 +144,12 @@ class DefaultNotificationDataFactory(
.getFallbackNotification(notificationAccountParams.user.userId) .getFallbackNotification(notificationAccountParams.user.userId)
?.notification ?.notification
val notification = notificationCreator.createFallbackNotification( val notification = notificationCreator.createFallbackNotification(
existingNotification, existingNotification = existingNotification,
notificationAccountParams, notificationAccountParams = notificationAccountParams,
fallback, fallbackNotifiableEvents = fallback,
) )
return OneShotNotification( return OneShotNotification(
tag = "FALLBACK", tag = FALLBACK_NOTIFICATION_TAG,
notification = notification, notification = notification,
isNoisy = false, isNoisy = false,
timestamp = fallback.first().timestamp timestamp = fallback.first().timestamp
@ -174,6 +174,10 @@ class DefaultNotificationDataFactory(
) )
} }
} }
companion object {
const val FALLBACK_NOTIFICATION_TAG = "FALLBACK"
}
} }
data class RoomNotification( data class RoomNotification(

View file

@ -15,10 +15,6 @@ 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.ThreadId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.currentRoomId
import io.element.android.services.appnavstate.api.currentSessionId
import io.element.android.services.appnavstate.api.currentThreadId
data class NotifiableMessageEvent( data class NotifiableMessageEvent(
override val sessionId: SessionId, override val sessionId: SessionId,
@ -56,24 +52,3 @@ data class NotifiableMessageEvent(
val imageUri: Uri? val imageUri: Uri?
get() = imageUriString?.toUri() get() = imageUriString?.toUri()
} }
/**
* Used to check if a notification should be ignored based on the current app and navigation state.
*/
fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationState): Boolean {
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
null -> false
else -> {
// Never ignore ringing call notifications
if (this is NotifiableRingingCallEvent) {
false
} else {
appNavigationState.isInForeground &&
sessionId == currentSessionId &&
roomId == currentRoomId &&
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
}
}
}
}

View file

@ -15,8 +15,11 @@ import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
@ -28,23 +31,26 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import io.element.android.services.appnavstate.test.aNavigationState import io.element.android.services.appnavstate.test.aNavigationState
import io.element.android.services.appnavstate.test.anAppNavigationState
import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -92,26 +98,25 @@ class DefaultNotificationDrawerManagerTest {
@Test @Test
fun `react to applicationStateChange`() = runTest { fun `react to applicationStateChange`() = runTest {
// For now just call all the API. Later, add more valuable tests. // For now just call all the API. Later, add more valuable tests.
val appNavigationStateFlow: MutableStateFlow<AppNavigationState> = MutableStateFlow( val appNavigationStateService = FakeAppNavigationStateService()
AppNavigationState(
navigationState = NavigationState.Root,
isInForeground = true,
)
)
val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow)
createDefaultNotificationDrawerManager( createDefaultNotificationDrawerManager(
appNavigationStateService = appNavigationStateService appNavigationStateService = appNavigationStateService
) )
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(), isInForeground = true))
runCurrent() runCurrent()
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true)) appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true))
runCurrent() runCurrent()
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_ID), isInForeground = true)) appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_ID), isInForeground = true))
runCurrent() runCurrent()
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true)) appNavigationStateService.emitNavigationState(
AppNavigationState(
aNavigationState(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID),
isInForeground = true
)
)
runCurrent() runCurrent()
// Like a user sign out // Like a user sign out
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) appNavigationStateService.emitNavigationState(AppNavigationState(aNavigationState(), isInForeground = true))
runCurrent() runCurrent()
} }
@ -234,6 +239,262 @@ class DefaultNotificationDrawerManagerTest {
listOf(value(null), value(summaryId)), listOf(value(null), value(summaryId)),
) )
} }
@Test
fun `when the application is in background, all events trigger a notification`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID),
isInForeground = false,
),
notifiableEvents = listOf(
aFallbackNotifiableEvent(sessionId = A_SESSION_ID),
aFallbackNotifiableEvent(sessionId = A_SESSION_ID_2),
anInviteNotifiableEvent(sessionId = A_SESSION_ID),
anInviteNotifiableEvent(sessionId = A_SESSION_ID_2),
aSimpleNotifiableEvent(sessionId = A_SESSION_ID),
aSimpleNotifiableEvent(sessionId = A_SESSION_ID_2),
aNotifiableMessageEvent(sessionId = A_SESSION_ID),
aNotifiableMessageEvent(sessionId = A_SESSION_ID_2),
aNotifiableMessageEvent(sessionId = A_SESSION_ID, threadId = A_THREAD_ID),
aNotifiableMessageEvent(sessionId = A_SESSION_ID_2, threadId = A_THREAD_ID_2),
),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 2,
)
@Test
fun `fallback event is ignored when the room list is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID),
),
notifiableEvents = listOf(aFallbackNotifiableEvent(sessionId = A_SESSION_ID)),
shouldEmitNotification = false,
)
@Test
fun `fallback event is not ignored when a room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
),
notifiableEvents = listOf(aFallbackNotifiableEvent(sessionId = A_SESSION_ID)),
shouldEmitNotification = true,
)
@Test
fun `fallback event for other session is not ignored when the room list is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID_2),
),
notifiableEvents = listOf(aFallbackNotifiableEvent(sessionId = A_SESSION_ID)),
shouldEmitNotification = true,
)
@Test
fun `invite notifiable event is emits a notification when the room list is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID),
),
notifiableEvents = listOf(anInviteNotifiableEvent(sessionId = A_SESSION_ID)),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `invite notifiable event does not emit a notification when the same room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
),
notifiableEvents = listOf(
anInviteNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
)
),
shouldEmitNotification = false,
)
@Test
fun `invite notifiable event emits a notification when another room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2),
),
notifiableEvents = listOf(
anInviteNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
)
),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `simple notifiable event is emits a notification when the room list is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID),
),
notifiableEvents = listOf(aSimpleNotifiableEvent(sessionId = A_SESSION_ID)),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `simple notifiable event does not emit a notification when the same room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
),
notifiableEvents = listOf(
aSimpleNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
)
),
shouldEmitNotification = false,
)
@Test
fun `simple notifiable event emits a notification when another room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2),
),
notifiableEvents = listOf(
aSimpleNotifiableEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
)
),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `notifiable event is emits a notification when the room list is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID),
),
notifiableEvents = listOf(aNotifiableMessageEvent(sessionId = A_SESSION_ID)),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `notifiable event does not emit a notification when the same room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
),
notifiableEvents = listOf(
aNotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
)
),
shouldEmitNotification = false,
)
@Test
fun `notifiable event for a thread emits a notification when the same room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID),
),
notifiableEvents = listOf(
aNotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `notifiable event for a thread does not emit a notification when the same thread is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID),
),
notifiableEvents = listOf(
aNotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
),
shouldEmitNotification = false,
)
@Test
fun `notifiable event for a thread emits a notification when another thread is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID_2),
),
notifiableEvents = listOf(
aNotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `notifiable event for a thread emits a notification when a thread of another room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2, threadId = A_THREAD_ID_2),
),
notifiableEvents = listOf(
aNotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = A_THREAD_ID,
)
),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
@Test
fun `notifiable event emits a notification when another room is displayed`() = testOnNotifiableEventReceived(
appNavigationState = anAppNavigationState(
navigationState = aNavigationState(sessionId = A_SESSION_ID, roomId = A_ROOM_ID_2),
),
notifiableEvents = listOf(
aNotifiableMessageEvent(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
)
),
shouldEmitNotification = true,
extraInvocationsForNotificationSummary = 1,
)
private fun testOnNotifiableEventReceived(
appNavigationState: AppNavigationState,
notifiableEvents: List<NotifiableEvent>,
shouldEmitNotification: Boolean,
extraInvocationsForNotificationSummary: Int = 0,
) = runTest {
val showNotificationResult = lambdaRecorder<String?, Int, Notification, Boolean> { _, _, _ ->
true
}
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
appNavigationStateService = FakeAppNavigationStateService(
initialAppNavigationState = appNavigationState,
),
notificationDisplayer = FakeNotificationDisplayer(
showNotificationResult = showNotificationResult,
)
)
defaultNotificationDrawerManager.onNotifiableEventsReceived(notifiableEvents)
showNotificationResult.assertions().isCalledExactly(
if (shouldEmitNotification) {
notifiableEvents.size + extraInvocationsForNotificationSummary
} else {
0
}
)
}
} }
fun TestScope.createDefaultNotificationDrawerManager( fun TestScope.createDefaultNotificationDrawerManager(
@ -251,7 +512,7 @@ fun TestScope.createDefaultNotificationDrawerManager(
return DefaultNotificationDrawerManager( return DefaultNotificationDrawerManager(
notificationDisplayer = notificationDisplayer, notificationDisplayer = notificationDisplayer,
notificationRenderer = notificationRenderer ?: NotificationRenderer( notificationRenderer = notificationRenderer ?: NotificationRenderer(
notificationDisplayer = FakeNotificationDisplayer(), notificationDisplayer = notificationDisplayer,
notificationDataFactory = DefaultNotificationDataFactory( notificationDataFactory = DefaultNotificationDataFactory(
notificationCreator = FakeNotificationCreator(), notificationCreator = FakeNotificationCreator(),
roomGroupMessageCreator = roomGroupMessageCreator, roomGroupMessageCreator = roomGroupMessageCreator,

View file

@ -144,8 +144,10 @@ fun aNotifiableCallEvent(
rtcNotificationType = rtcNotificationType, rtcNotificationType = rtcNotificationType,
) )
fun aFallbackNotifiableEvent() = FallbackNotifiableEvent( fun aFallbackNotifiableEvent(
sessionId = A_SESSION_ID, sessionId: SessionId = A_SESSION_ID,
) = FallbackNotifiableEvent(
sessionId = sessionId,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
editedEventId = null, editedEventId = null,

View file

@ -43,9 +43,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
runCurrent() runCurrent()
// Make sure it's warm by changing its internal state // Make sure it's warm by changing its internal state
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false)) navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
runCurrent() runCurrent()
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true)) navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
runCurrent() runCurrent()
// The transaction should be present now // The transaction should be present now
@ -63,9 +63,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
@Test @Test
fun `Opening the app in a cold state does nothing`() = runTest { fun `Opening the app in a cold state does nothing`() = runTest {
val navigationStateService = FakeAppNavigationStateService().apply { val navigationStateService = FakeAppNavigationStateService(
appNavigationState.emit(AppNavigationState(NavigationState.Root, false)) initialAppNavigationState = AppNavigationState(NavigationState.Root, false)
} )
val roomListService = FakeRoomListService().apply { val roomListService = FakeRoomListService().apply {
postState(RoomListService.State.Idle) postState(RoomListService.State.Idle)
} }
@ -110,9 +110,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
runCurrent() runCurrent()
// Make sure it's warm by changing its internal state // Make sure it's warm by changing its internal state
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false)) navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
runCurrent() runCurrent()
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true)) navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
runCurrent() runCurrent()
// The transaction should be present now // The transaction should be present now
@ -145,9 +145,9 @@ class DefaultAnalyticsRoomListStateWatcherTest {
runCurrent() runCurrent()
// Make sure it's warm by changing its internal state // Make sure it's warm by changing its internal state
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false)) navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = false))
runCurrent() runCurrent()
navigationStateService.appNavigationState.emit(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true)) navigationStateService.emitNavigationState(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true))
runCurrent() runCurrent()
// The transaction was never added // The transaction was never added

View file

@ -31,7 +31,6 @@ import io.sentry.Sentry
import io.sentry.SentryTracer import io.sentry.SentryTracer
import io.sentry.protocol.SentryId import io.sentry.protocol.SentryId
import io.sentry.protocol.SentryTransaction import io.sentry.protocol.SentryTransaction
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -149,7 +148,7 @@ class SentryAnalyticsProviderTest {
) )
}, },
appNavigationStateService = FakeAppNavigationStateService( appNavigationStateService = FakeAppNavigationStateService(
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)) initialAppNavigationState = AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)
) )
).run { ).run {
init() init()
@ -182,7 +181,7 @@ class SentryAnalyticsProviderTest {
) )
}, },
appNavigationStateService = FakeAppNavigationStateService( appNavigationStateService = FakeAppNavigationStateService(
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Root, isInForeground = true)) initialAppNavigationState = AppNavigationState(navigationState = NavigationState.Root, isInForeground = true)
) )
).run { ).run {
init() init()
@ -203,7 +202,7 @@ class SentryAnalyticsProviderTest {
) )
}, },
appNavigationStateService = FakeAppNavigationStateService( appNavigationStateService = FakeAppNavigationStateService(
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)) initialAppNavigationState = AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)
) )
).run { ).run {
init() init()
@ -221,7 +220,7 @@ class SentryAnalyticsProviderTest {
buildMeta: BuildMeta = aBuildMeta(), buildMeta: BuildMeta = aBuildMeta(),
getDatabaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) }, getDatabaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
appNavigationStateService: FakeAppNavigationStateService = FakeAppNavigationStateService( appNavigationStateService: FakeAppNavigationStateService = FakeAppNavigationStateService(
MutableStateFlow(AppNavigationState(navigationState = NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)) initialAppNavigationState = AppNavigationState(NavigationState.Session("owner", A_SESSION_ID), isInForeground = true)
) )
) = SentryAnalyticsProvider( ) = SentryAnalyticsProvider(
context = InstrumentationRegistry.getInstrumentation().targetContext, context = InstrumentationRegistry.getInstrumentation().targetContext,

View file

@ -11,6 +11,7 @@ package io.element.android.services.appnavstate.test
import io.element.android.libraries.matrix.api.core.RoomId 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.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.NavigationState
const val A_SESSION_OWNER = "aSessionOwner" const val A_SESSION_OWNER = "aSessionOwner"
@ -35,3 +36,11 @@ fun aNavigationState(
} }
return NavigationState.Thread(A_THREAD_OWNER, threadId, room) return NavigationState.Thread(A_THREAD_OWNER, threadId, room)
} }
fun anAppNavigationState(
navigationState: NavigationState = aNavigationState(),
isInForeground: Boolean = true,
) = AppNavigationState(
navigationState = navigationState,
isInForeground = isInForeground,
)

View file

@ -15,15 +15,21 @@ import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.NavigationState
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeAppNavigationStateService( class FakeAppNavigationStateService(
override val appNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow( initialAppNavigationState: AppNavigationState = AppNavigationState(
AppNavigationState( navigationState = NavigationState.Root,
navigationState = NavigationState.Root, isInForeground = true,
isInForeground = true,
)
), ),
) : AppNavigationStateService { ) : AppNavigationStateService {
private val _appNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(initialAppNavigationState)
override val appNavigationState = _appNavigationState.asStateFlow()
fun emitNavigationState(state: AppNavigationState) {
_appNavigationState.value = state
}
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
override fun onLeavingSession(owner: String) = Unit override fun onLeavingSession(owner: String) = Unit