Skip to content

Commit fe7e67c

Browse files
committed
Notification: implement a counter in the fallback notification.
1 parent cd9a1fe commit fe7e67c

19 files changed

+126
-97
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface ActiveNotificationsProvider {
3434
fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification>
3535
fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
3636
fun getSummaryNotification(sessionId: SessionId): StatusBarNotification?
37+
fun getFallbackNotification(sessionId: SessionId): StatusBarNotification?
3738
fun count(sessionId: SessionId): Int
3839
}
3940

@@ -76,6 +77,11 @@ class DefaultActiveNotificationsProvider(
7677
return getNotificationsForSession(sessionId).find { it.id == summaryId }
7778
}
7879

80+
override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? {
81+
val fallbackId = NotificationIdProvider.getFallbackNotificationId(sessionId)
82+
return getNotificationsForSession(sessionId).find { it.id == fallbackId }
83+
}
84+
7985
override fun count(sessionId: SessionId): Int {
8086
return getNotificationsForSession(sessionId).size
8187
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,12 @@ import dev.zacsweers.metro.Inject
1212
import io.element.android.libraries.matrix.api.core.EventId
1313
import io.element.android.libraries.matrix.api.core.RoomId
1414
import io.element.android.libraries.matrix.api.core.SessionId
15-
import io.element.android.libraries.push.impl.R
1615
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
17-
import io.element.android.services.toolbox.api.strings.StringProvider
1816
import io.element.android.services.toolbox.api.systemclock.SystemClock
1917

2018
@Inject
2119
class FallbackNotificationFactory(
2220
private val clock: SystemClock,
23-
private val stringProvider: StringProvider,
2421
) {
2522
fun create(
2623
sessionId: SessionId,
@@ -36,7 +33,7 @@ class FallbackNotificationFactory(
3633
isRedacted = false,
3734
isUpdated = false,
3835
timestamp = clock.epochMillis(),
39-
description = stringProvider.getString(R.string.notification_fallback_content),
36+
description = "",
4037
cause = cause,
4138
)
4239
}

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

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
2121
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
2222
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
2323
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
24-
import io.element.android.services.toolbox.api.strings.StringProvider
2524

2625
interface NotificationDataFactory {
2726
suspend fun toNotifications(
@@ -46,16 +45,15 @@ interface NotificationDataFactory {
4645

4746
@JvmName("toNotificationFallbackEvents")
4847
@Suppress("INAPPLICABLE_JVM_NAME")
49-
fun toNotifications(
48+
fun toNotification(
5049
fallback: List<FallbackNotifiableEvent>,
5150
notificationAccountParams: NotificationAccountParams,
52-
): List<OneShotNotification>
51+
): OneShotNotification?
5352

5453
fun createSummaryNotification(
5554
roomNotifications: List<RoomNotification>,
5655
invitationNotifications: List<OneShotNotification>,
5756
simpleNotifications: List<OneShotNotification>,
58-
fallbackNotifications: List<OneShotNotification>,
5957
notificationAccountParams: NotificationAccountParams,
6058
): SummaryNotification
6159
}
@@ -66,7 +64,6 @@ class DefaultNotificationDataFactory(
6664
private val roomGroupMessageCreator: RoomGroupMessageCreator,
6765
private val summaryGroupMessageCreator: SummaryGroupMessageCreator,
6866
private val activeNotificationsProvider: ActiveNotificationsProvider,
69-
private val stringProvider: StringProvider,
7067
) : NotificationDataFactory {
7168
override suspend fun toNotifications(
7269
messages: List<NotifiableMessageEvent>,
@@ -141,25 +138,31 @@ class DefaultNotificationDataFactory(
141138

142139
@JvmName("toNotificationFallbackEvents")
143140
@Suppress("INAPPLICABLE_JVM_NAME")
144-
override fun toNotifications(
141+
override fun toNotification(
145142
fallback: List<FallbackNotifiableEvent>,
146143
notificationAccountParams: NotificationAccountParams,
147-
): List<OneShotNotification> {
148-
return fallback.map { event ->
149-
OneShotNotification(
150-
tag = event.eventId.value,
151-
notification = notificationCreator.createFallbackNotification(notificationAccountParams, event),
152-
isNoisy = false,
153-
timestamp = event.timestamp
154-
)
155-
}
144+
): OneShotNotification? {
145+
if (fallback.isEmpty()) return null
146+
val existingNotification = activeNotificationsProvider
147+
.getFallbackNotification(notificationAccountParams.user.userId)
148+
?.notification
149+
val notification = notificationCreator.createFallbackNotification(
150+
existingNotification,
151+
notificationAccountParams,
152+
fallback,
153+
)
154+
return OneShotNotification(
155+
tag = "FALLBACK",
156+
notification = notification,
157+
isNoisy = false,
158+
timestamp = fallback.first().timestamp
159+
)
156160
}
157161

158162
override fun createSummaryNotification(
159163
roomNotifications: List<RoomNotification>,
160164
invitationNotifications: List<OneShotNotification>,
161165
simpleNotifications: List<OneShotNotification>,
162-
fallbackNotifications: List<OneShotNotification>,
163166
notificationAccountParams: NotificationAccountParams,
164167
): SummaryNotification {
165168
return when {
@@ -169,7 +172,6 @@ class DefaultNotificationDataFactory(
169172
roomNotifications = roomNotifications,
170173
invitationNotifications = invitationNotifications,
171174
simpleNotifications = simpleNotifications,
172-
fallbackNotifications = fallbackNotifications,
173175
notificationAccountParams = notificationAccountParams,
174176
)
175177
)

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,11 @@ class NotificationRenderer(
5555
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, imageLoader, notificationAccountParams)
5656
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, notificationAccountParams)
5757
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, notificationAccountParams)
58-
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, notificationAccountParams)
58+
val fallbackNotification = notificationDataFactory.toNotification(groupedEvents.fallbackEvents, notificationAccountParams)
5959
val summaryNotification = notificationDataFactory.createSummaryNotification(
6060
roomNotifications = roomNotifications,
6161
invitationNotifications = invitationNotifications,
6262
simpleNotifications = simpleNotifications,
63-
fallbackNotifications = fallbackNotifications,
6463
notificationAccountParams = notificationAccountParams,
6564
)
6665

@@ -107,13 +106,12 @@ class NotificationRenderer(
107106
}
108107
}
109108

110-
// Show only the first fallback notification
111-
if (fallbackNotifications.isNotEmpty()) {
112-
Timber.tag(loggerTag.value).d("Showing fallback notification")
109+
if (fallbackNotification != null) {
110+
Timber.tag(loggerTag.value).d("Showing or updating fallback notification")
113111
notificationDisplayer.showNotification(
114-
tag = "FALLBACK",
112+
tag = fallbackNotification.tag,
115113
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
116-
notification = fallbackNotifications.first().notification
114+
notification = fallbackNotification.notification,
117115
)
118116
}
119117

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ interface SummaryGroupMessageCreator {
2222
roomNotifications: List<RoomNotification>,
2323
invitationNotifications: List<OneShotNotification>,
2424
simpleNotifications: List<OneShotNotification>,
25-
fallbackNotifications: List<OneShotNotification>,
2625
): Notification
2726
}
2827

@@ -45,7 +44,6 @@ class DefaultSummaryGroupMessageCreator(
4544
roomNotifications: List<RoomNotification>,
4645
invitationNotifications: List<OneShotNotification>,
4746
simpleNotifications: List<OneShotNotification>,
48-
fallbackNotifications: List<OneShotNotification>,
4947
): Notification {
5048
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
5149
invitationNotifications.any { it.isNoisy } ||

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

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ interface NotificationCreator {
7575
): Notification
7676

7777
fun createFallbackNotification(
78+
existingNotification: Notification?,
7879
notificationAccountParams: NotificationAccountParams,
79-
fallbackNotifiableEvent: FallbackNotifiableEvent,
80+
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
8081
): Notification
8182

8283
/**
@@ -240,11 +241,13 @@ class DefaultNotificationCreator(
240241
.addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
241242
.addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
242243
// Build the pending intent for when the notification is clicked
243-
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(
244-
sessionId = inviteNotifiableEvent.sessionId,
245-
roomId = inviteNotifiableEvent.roomId,
246-
eventId = null,
247-
))
244+
.setContentIntent(
245+
pendingIntentFactory.createOpenRoomPendingIntent(
246+
sessionId = inviteNotifiableEvent.sessionId,
247+
roomId = inviteNotifiableEvent.roomId,
248+
eventId = null,
249+
)
250+
)
248251
.apply {
249252
if (inviteNotifiableEvent.noisy) {
250253
// Compat
@@ -276,12 +279,14 @@ class DefaultNotificationCreator(
276279
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
277280
.configureWith(notificationAccountParams)
278281
.setAutoCancel(true)
279-
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(
280-
sessionId = simpleNotifiableEvent.sessionId,
281-
roomId = simpleNotifiableEvent.roomId,
282-
eventId = null,
283-
extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true),
284-
))
282+
.setContentIntent(
283+
pendingIntentFactory.createOpenRoomPendingIntent(
284+
sessionId = simpleNotifiableEvent.sessionId,
285+
roomId = simpleNotifiableEvent.roomId,
286+
eventId = null,
287+
extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true),
288+
)
289+
)
285290
.apply {
286291
if (simpleNotifiableEvent.noisy) {
287292
// Compat
@@ -295,28 +300,35 @@ class DefaultNotificationCreator(
295300
}
296301

297302
override fun createFallbackNotification(
303+
existingNotification: Notification?,
298304
notificationAccountParams: NotificationAccountParams,
299-
fallbackNotifiableEvent: FallbackNotifiableEvent,
305+
fallbackNotifiableEvents: List<FallbackNotifiableEvent>,
300306
): Notification {
301307
val channelId = notificationChannels.getChannelIdForMessage(false)
308+
val existingCounter = existingNotification
309+
?.extras
310+
?.getInt(FALLBACK_COUNTER_EXTRA)
311+
?: 0
312+
val counter = existingCounter + fallbackNotifiableEvents.size
313+
val fallbackNotifiableEvent = fallbackNotifiableEvents.first()
302314
return NotificationCompat.Builder(context, channelId)
303315
.setOnlyAlertOnce(true)
304316
.setContentTitle(buildMeta.applicationName.annotateForDebug(7))
305-
.setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8))
317+
.setContentText(
318+
stringProvider.getQuantityString(R.plurals.notification_fallback_n_content, counter, counter)
319+
.annotateForDebug(8)
320+
)
321+
.setExtras(
322+
bundleOf(
323+
FALLBACK_COUNTER_EXTRA to counter
324+
)
325+
)
326+
.setNumber(counter)
306327
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
307328
.configureWith(notificationAccountParams)
308329
.setAutoCancel(true)
309330
.setWhen(fallbackNotifiableEvent.timestamp)
310-
// Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite
311-
// and the user won't have access to the room yet, resulting in an error screen.
312331
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId))
313-
.setDeleteIntent(
314-
pendingIntentFactory.createDismissEventPendingIntent(
315-
fallbackNotifiableEvent.sessionId,
316-
fallbackNotifiableEvent.roomId,
317-
fallbackNotifiableEvent.eventId
318-
)
319-
)
320332
.setPriority(NotificationCompat.PRIORITY_LOW)
321333
.build()
322334
}
@@ -503,6 +515,7 @@ class DefaultNotificationCreator(
503515

504516
companion object {
505517
const val MESSAGE_EVENT_ID = "message_event_id"
518+
private const val FALLBACK_COUNTER_EXTRA = "COUNTER"
506519
}
507520
}
508521

libraries/push/impl/src/main/res/values/localazy.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
</plurals>
1616
<string name="notification_error_unified_push_unregistered_android">"The UnifiedPush notification distributor couldn\'t be registered, so you will not receive notifications anymore. Please check the notifications settings of the app and the status of the push distributor."</string>
1717
<string name="notification_fallback_content">"You have new messages."</string>
18+
<plurals name="notification_fallback_n_content">
19+
<item quantity="one">"You have %d new message."</item>
20+
<item quantity="other">"You have %d new messages."</item>
21+
</plurals>
1822
<string name="notification_incoming_call">"📹 Incoming call"</string>
1923
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
2024
<string name="notification_invitation_action_join">"Join"</string>

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,19 @@ class DefaultActiveNotificationsProviderTest {
153153
assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID_2)).isNull()
154154
}
155155

156+
@Test
157+
fun `getFallbackNotification returns only the fallback notification for that session id if it exists`() {
158+
val activeNotifications = listOf(
159+
aStatusBarNotification(id = notificationIdProvider.getFallbackNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
160+
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
161+
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
162+
)
163+
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
164+
165+
assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID)).isNotNull()
166+
assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID_2)).isNull()
167+
}
168+
156169
private fun aStatusBarNotification(id: Int, groupId: String, tag: String? = null) = mockk<StatusBarNotification> {
157170
every { this@mockk.id } returns id
158171
every { this@mockk.tag } returns tag

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess
5656
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
5757
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
5858
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
59-
import io.element.android.services.toolbox.test.strings.FakeStringProvider
6059
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
6160
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
6261
import kotlinx.coroutines.test.runTest
@@ -663,7 +662,7 @@ class DefaultNotifiableEventResolverTest {
663662
roomId = A_ROOM_ID,
664663
eventId = AN_EVENT_ID,
665664
editedEventId = null,
666-
description = "You have new messages.",
665+
description = "",
667666
canBeReplaced = true,
668667
isRedacted = false,
669668
isUpdated = false,
@@ -895,7 +894,6 @@ class DefaultNotifiableEventResolverTest {
895894
callNotificationEventResolver = callNotificationEventResolver,
896895
fallbackNotificationFactory = FallbackNotificationFactory(
897896
clock = FakeSystemClock(),
898-
stringProvider = FakeStringProvider(defaultResult = "You have new messages.")
899897
),
900898
featureFlagService = FakeFeatureFlagService(),
901899
)

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService
3737
import io.element.android.services.appnavstate.api.NavigationState
3838
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
3939
import io.element.android.services.appnavstate.test.aNavigationState
40-
import io.element.android.services.toolbox.test.strings.FakeStringProvider
4140
import io.element.android.tests.testutils.lambda.any
4241
import io.element.android.tests.testutils.lambda.lambdaRecorder
4342
import io.element.android.tests.testutils.lambda.value
@@ -224,7 +223,6 @@ class DefaultNotificationDrawerManagerTest {
224223
roomGroupMessageCreator = roomGroupMessageCreator,
225224
summaryGroupMessageCreator = summaryGroupMessageCreator,
226225
activeNotificationsProvider = activeNotificationsProvider,
227-
stringProvider = FakeStringProvider(),
228226
),
229227
enterpriseService = enterpriseService,
230228
sessionStore = sessionStore,

0 commit comments

Comments
 (0)