Skip to content

Commit 9247cd7

Browse files
authored
Fix: make sure we ignore notifications for open rooms (#867)
* Make sure we ignore notifications for open rooms - Listen to process lifecycle changes in `AppForegroundStateService`. Use initializers to reliable create it. - Merge `AppNavigationState` with `AppForegroundState`. Renamed the previous `AppNavigationState` to `NavigationState`, created a new `AppNavigationState` which contains both the navigation state and the foreground state.
1 parent 004b86b commit 9247cd7

File tree

26 files changed

+552
-246
lines changed

26 files changed

+552
-246
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ dependencies {
209209
implementation(libs.androidx.core)
210210
implementation(libs.androidx.corektx)
211211
implementation(libs.androidx.lifecycle.runtime)
212+
implementation(libs.androidx.lifecycle.process)
212213
implementation(libs.androidx.activity.compose)
213214
implementation(libs.androidx.startup)
214215
implementation(libs.androidx.preference)

app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232
android:theme="@style/Theme.ElementX"
3333
tools:targetApi="33">
3434

35+
<provider
36+
android:name="androidx.startup.InitializationProvider"
37+
android:authorities="${applicationId}.androidx-startup"
38+
android:exported="false"
39+
tools:node="merge">
40+
41+
<meta-data
42+
android:name='androidx.lifecycle.ProcessLifecycleInitializer'
43+
android:value='androidx.startup' />
44+
45+
</provider>
46+
3547
<activity
3648
android:name=".MainActivity"
3749
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"

appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
3232
import io.element.android.libraries.architecture.childNode
3333
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
3434
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
35-
import io.element.android.services.appnavstate.test.NoopAppNavigationStateService
35+
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
3636
import org.junit.Rule
3737
import org.junit.Test
3838

@@ -82,7 +82,7 @@ class RoomFlowNodeTest {
8282
plugins = plugins,
8383
messagesEntryPoint = messagesEntryPoint,
8484
roomDetailsEntryPoint = roomDetailsEntryPoint,
85-
appNavigationStateService = NoopAppNavigationStateService(),
85+
appNavigationStateService = FakeAppNavigationStateService(),
8686
roomMembershipObserver = RoomMembershipObserver()
8787
)
8888

libraries/push/impl/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ dependencies {
3131
implementation(libs.dagger)
3232
implementation(libs.androidx.corektx)
3333
implementation(libs.androidx.datastore.preferences)
34-
implementation(libs.androidx.lifecycle.process)
3534
implementation(libs.androidx.security.crypto)
3635
implementation(libs.network.retrofit)
3736
implementation(libs.serialization.json)

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

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
3232
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
3333
import io.element.android.libraries.push.api.store.PushDataStore
3434
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
35-
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
36-
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
37-
import io.element.android.services.appnavstate.api.AppNavigationState
35+
import io.element.android.services.appnavstate.api.NavigationState
3836
import io.element.android.services.appnavstate.api.AppNavigationStateService
3937
import kotlinx.coroutines.CoroutineScope
4038
import kotlinx.coroutines.delay
@@ -65,7 +63,6 @@ class DefaultNotificationDrawerManager @Inject constructor(
6563
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
6664
*/
6765
private val notificationState by lazy { createInitialNotificationState() }
68-
private var currentAppNavigationState: AppNavigationState? = null
6966
private val firstThrottler = FirstThrottler(200)
7067

7168
// TODO EAx add a setting per user for this
@@ -74,26 +71,25 @@ class DefaultNotificationDrawerManager @Inject constructor(
7471
init {
7572
// Observe application state
7673
coroutineScope.launch {
77-
appNavigationStateService.appNavigationStateFlow
78-
.collect { onAppNavigationStateChange(it) }
74+
appNavigationStateService.appNavigationState
75+
.collect { onAppNavigationStateChange(it.navigationState) }
7976
}
8077
}
8178

82-
private fun onAppNavigationStateChange(appNavigationState: AppNavigationState) {
83-
currentAppNavigationState = appNavigationState
84-
when (appNavigationState) {
85-
AppNavigationState.Root -> {}
86-
is AppNavigationState.Session -> {}
87-
is AppNavigationState.Space -> {}
88-
is AppNavigationState.Room -> {
79+
private fun onAppNavigationStateChange(navigationState: NavigationState) {
80+
when (navigationState) {
81+
NavigationState.Root -> {}
82+
is NavigationState.Session -> {}
83+
is NavigationState.Space -> {}
84+
is NavigationState.Room -> {
8985
// Cleanup notification for current room
90-
clearMessagesForRoom(appNavigationState.parentSpace.parentSession.sessionId, appNavigationState.roomId)
86+
clearMessagesForRoom(navigationState.parentSpace.parentSession.sessionId, navigationState.roomId)
9187
}
92-
is AppNavigationState.Thread -> {
88+
is NavigationState.Thread -> {
9389
onEnteringThread(
94-
appNavigationState.parentRoom.parentSpace.parentSession.sessionId,
95-
appNavigationState.parentRoom.roomId,
96-
appNavigationState.threadId
90+
navigationState.parentRoom.parentSpace.parentSession.sessionId,
91+
navigationState.parentRoom.roomId,
92+
navigationState.threadId
9793
)
9894
}
9995
}
@@ -225,7 +221,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
225221
private suspend fun refreshNotificationDrawerBg() {
226222
Timber.v("refreshNotificationDrawerBg()")
227223
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
228-
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also {
224+
notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also {
229225
queuedEvents.clearAndAdd(it.onlyKeptEvents())
230226
}
231227
}
@@ -275,8 +271,4 @@ class DefaultNotificationDrawerManager @Inject constructor(
275271
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
276272
}
277273
}
278-
279-
fun shouldIgnoreMessageEventInRoom(resolvedEvent: NotifiableMessageEvent): Boolean {
280-
return resolvedEvent.shouldIgnoreEventInRoom(currentAppNavigationState)
281-
}
282274
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,27 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
2323
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
2424
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
2525
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
26-
import io.element.android.services.appnavstate.api.AppNavigationState
26+
import io.element.android.services.appnavstate.api.AppNavigationStateService
2727
import timber.log.Timber
2828
import javax.inject.Inject
2929

3030
private typealias ProcessedEvents = List<ProcessedEvent<NotifiableEvent>>
3131

3232
class NotifiableEventProcessor @Inject constructor(
3333
private val outdatedDetector: OutdatedEventDetector,
34+
private val appNavigationStateService: AppNavigationStateService,
3435
) {
3536

3637
fun process(
3738
queuedEvents: List<NotifiableEvent>,
38-
appNavigationState: AppNavigationState?,
3939
renderedEvents: ProcessedEvents,
4040
): ProcessedEvents {
41+
val appState = appNavigationStateService.appNavigationState.value
4142
val processedEvents = queuedEvents.map {
4243
val type = when (it) {
4344
is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP
4445
is NotifiableMessageEvent -> when {
45-
it.shouldIgnoreEventInRoom(appNavigationState) -> {
46+
it.shouldIgnoreEventInRoom(appState) -> {
4647
ProcessedEvent.Type.REMOVE
4748
.also { Timber.d("notification message removed due to currently viewing the same room or thread") }
4849
}
@@ -55,7 +56,7 @@ class NotifiableEventProcessor @Inject constructor(
5556
else -> ProcessedEvent.Type.KEEP
5657
}
5758
is FallbackNotifiableEvent -> when {
58-
it.shouldIgnoreEventInRoom(appNavigationState) -> {
59+
it.shouldIgnoreEventInRoom(appState) -> {
5960
ProcessedEvent.Type.REMOVE
6061
.also { Timber.d("notification fallback removed due to currently viewing the same room or thread") }
6162
}

libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
package io.element.android.libraries.push.impl.notifications.model
1717

1818
import android.net.Uri
19-
import androidx.lifecycle.Lifecycle
20-
import androidx.lifecycle.ProcessLifecycleOwner
2119
import io.element.android.libraries.matrix.api.core.EventId
2220
import io.element.android.libraries.matrix.api.core.RoomId
2321
import io.element.android.libraries.matrix.api.core.SessionId
@@ -69,18 +67,13 @@ data class NotifiableMessageEvent(
6967
/**
7068
* Used to check if a notification should be ignored based on the current app and navigation state.
7169
*/
72-
fun NotifiableEvent.shouldIgnoreEventInRoom(
73-
appNavigationState: AppNavigationState?
74-
): Boolean {
75-
val currentSessionId = appNavigationState?.currentSessionId() ?: return false
76-
return when (val currentRoomId = appNavigationState.currentRoomId()) {
70+
fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationState): Boolean {
71+
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
72+
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
7773
null -> false
78-
else -> isAppInForeground
74+
else -> appNavigationState.isInForeground
7975
&& sessionId == currentSessionId
8076
&& roomId == currentRoomId
81-
&& (this as? NotifiableMessageEvent)?.threadId == appNavigationState.currentThreadId()
77+
&& (this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
8278
}
8379
}
84-
85-
private val isAppInForeground: Boolean
86-
get() = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)

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

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,30 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable
3030
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
3131
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
3232
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
33-
import io.element.android.services.appnavstate.test.anAppNavigationState
33+
import io.element.android.services.appnavstate.api.NavigationState
34+
import io.element.android.services.appnavstate.api.AppNavigationState
35+
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
36+
import io.element.android.services.appnavstate.test.aNavigationState
37+
import kotlinx.coroutines.flow.MutableStateFlow
3438
import org.junit.Test
3539

36-
private val NOT_VIEWING_A_ROOM = anAppNavigationState()
37-
private val VIEWING_A_ROOM = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID)
38-
private val VIEWING_A_THREAD = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
40+
private val NOT_VIEWING_A_ROOM = aNavigationState()
41+
private val VIEWING_A_ROOM = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID)
42+
private val VIEWING_A_THREAD = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
3943

4044
class NotifiableEventProcessorTest {
4145

4246
private val outdatedDetector = FakeOutdatedEventDetector()
43-
private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance)
4447

4548
@Test
4649
fun `given simple events when processing then keep simple events`() {
4750
val events = listOf(
4851
aSimpleNotifiableEvent(eventId = AN_EVENT_ID),
4952
aSimpleNotifiableEvent(eventId = AN_EVENT_ID_2)
5053
)
54+
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
5155

52-
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
56+
val result = eventProcessor.process(events, renderedEvents = emptyList())
5357

5458
assertThat(result).isEqualTo(
5559
listOfProcessedEvents(
@@ -62,8 +66,9 @@ class NotifiableEventProcessorTest {
6266
@Test
6367
fun `given redacted simple event when processing then remove redaction event`() {
6468
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = EventType.REDACTION))
69+
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
6570

66-
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
71+
val result = eventProcessor.process(events, renderedEvents = emptyList())
6772

6873
assertThat(result).isEqualTo(
6974
listOfProcessedEvents(
@@ -78,8 +83,9 @@ class NotifiableEventProcessorTest {
7883
anInviteNotifiableEvent(roomId = A_ROOM_ID),
7984
anInviteNotifiableEvent(roomId = A_ROOM_ID_2)
8085
)
86+
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
8187

82-
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
88+
val result = eventProcessor.process(events, renderedEvents = emptyList())
8389

8490
assertThat(result).isEqualTo(
8591
listOfProcessedEvents(
@@ -94,7 +100,9 @@ class NotifiableEventProcessorTest {
94100
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
95101
outdatedDetector.givenEventIsOutOfDate(events[0])
96102

97-
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
103+
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
104+
105+
val result = eventProcessor.process(events, renderedEvents = emptyList())
98106

99107
assertThat(result).isEqualTo(
100108
listOfProcessedEvents(
@@ -107,8 +115,9 @@ class NotifiableEventProcessorTest {
107115
fun `given in date message event when processing then keep message event`() {
108116
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
109117
outdatedDetector.givenEventIsInDate(events[0])
118+
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
110119

111-
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
120+
val result = eventProcessor.process(events, renderedEvents = emptyList())
112121

113122
assertThat(result).isEqualTo(
114123
listOfProcessedEvents(
@@ -121,8 +130,9 @@ class NotifiableEventProcessorTest {
121130
fun `given viewing the same room main timeline when processing main timeline message event then removes message`() {
122131
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null))
123132
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
133+
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
124134

125-
val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList())
135+
val result = eventProcessor.process(events, renderedEvents = emptyList())
126136

127137
assertThat(result).isEqualTo(
128138
listOfProcessedEvents(
@@ -135,8 +145,9 @@ class NotifiableEventProcessorTest {
135145
fun `given viewing the same thread timeline when processing thread message event then removes message`() {
136146
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
137147
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
148+
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
138149

139-
val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList())
150+
val result = eventProcessor.process(events, renderedEvents = emptyList())
140151

141152
assertThat(result).isEqualTo(
142153
listOfProcessedEvents(
@@ -149,8 +160,9 @@ class NotifiableEventProcessorTest {
149160
fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() {
150161
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
151162
outdatedDetector.givenEventIsInDate(events[0])
163+
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
152164

153-
val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList())
165+
val result = eventProcessor.process(events, renderedEvents = emptyList())
154166

155167
assertThat(result).isEqualTo(
156168
listOfProcessedEvents(
@@ -163,8 +175,9 @@ class NotifiableEventProcessorTest {
163175
fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() {
164176
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
165177
outdatedDetector.givenEventIsInDate(events[0])
178+
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
166179

167-
val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList())
180+
val result = eventProcessor.process(events, renderedEvents = emptyList())
168181

169182
assertThat(result).isEqualTo(
170183
listOfProcessedEvents(
@@ -180,8 +193,9 @@ class NotifiableEventProcessorTest {
180193
ProcessedEvent(ProcessedEvent.Type.KEEP, events[0]),
181194
ProcessedEvent(ProcessedEvent.Type.KEEP, anInviteNotifiableEvent(eventId = AN_EVENT_ID_2))
182195
)
196+
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
183197

184-
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = renderedEvents)
198+
val result = eventProcessor.process(events, renderedEvents = renderedEvents)
185199

186200
assertThat(result).isEqualTo(
187201
listOfProcessedEvents(
@@ -194,4 +208,14 @@ class NotifiableEventProcessorTest {
194208
private fun listOfProcessedEvents(vararg event: Pair<ProcessedEvent.Type, NotifiableEvent>) = event.map {
195209
ProcessedEvent(it.first, it.second)
196210
}
211+
212+
private fun createProcessor(
213+
isInForeground: Boolean = false,
214+
navigationState: NavigationState
215+
): NotifiableEventProcessor {
216+
return NotifiableEventProcessor(
217+
outdatedDetector.instance,
218+
FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))),
219+
)
220+
}
197221
}

services/appnavstate/api/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ android {
2424

2525
dependencies {
2626
implementation(libs.coroutines.core)
27+
implementation(libs.androidx.lifecycle.runtime)
28+
implementation(libs.androidx.lifecycle.process)
29+
implementation(libs.androidx.startup)
2730
implementation(projects.libraries.matrix.api)
2831
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.services.appnavstate.api
18+
19+
import kotlinx.coroutines.flow.StateFlow
20+
21+
/**
22+
* A service that tracks the foreground state of the app.
23+
*/
24+
interface AppForegroundStateService {
25+
/**
26+
* Any updates to the foreground state of the app will be emitted here.
27+
*/
28+
val isInForeground: StateFlow<Boolean>
29+
30+
/**
31+
* Start observing the foreground state.
32+
*/
33+
fun start()
34+
}

0 commit comments

Comments
 (0)