Skip to content

Notification events resolving and rendering in batches #4722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from

Conversation

jmartinesp
Copy link
Member

@jmartinesp jmartinesp commented May 14, 2025

Content

  • Added NotificationResolverQueue type that enqueues notification fetching requests received in a certain time window, and resolves them in batches, emitting the results when done.
  • DefaultPushHandler now subscribes to these results and also processes them in batches, categorising the items and redirecting them to their callbacks (onRedactedEventReceived, handleRingingCallEvent, etc.).
  • DefaultNotificationDrawerManager now has a onNotifiableEventsReceived to render several notifications at the same time.

Motivation and context

The latest SDK version allows clients to fetch several notifications as a group so we need to run a single sliding sync request for them. However, since the items were returned at the same time and immediately sent to the NotificationManager so they can be displayed, the OS thought we were spamming the notifications and 'muted' (or discarded) some of them. Instead, we had to rework how we process the resolved notifications.

Tests

  • Send several messages in a very small time window in a room. If done correctly, both messages should be added to the notification at the same time.
  • Redact some of those messages, it should display as 'Redacted message' or something similar in the notification drawer.
  • Start a call in a DM, you should receive the ringing call notification with no issues.

Tested devices

  • Physical
  • Emulator
  • OS version(s): 14

Checklist

  • Changes have been tested on an Android device or Android emulator with API 24
  • UI change has been tested on both light and dark themes
  • Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
  • Pull request is based on the develop branch
  • Pull request title will be used in the release note, it clearly define what will change for the user
  • Pull request includes screenshots or videos if containing UI changes
  • You've made a self review of your PR

@jmartinesp jmartinesp changed the title Notification batching WIP: Notification batching May 14, 2025
Copy link
Contributor

github-actions bot commented May 14, 2025

📱 Scan the QR code below to install the build (arm64 only) for this PR.
QR code
If you can't scan the QR code you can install the build via this link: https://i.diawi.com/fmLfee

Copy link

codecov bot commented May 15, 2025

Codecov Report

Attention: Patch coverage is 76.84211% with 44 lines in your changes missing coverage. Please review.

Project coverage is 80.10%. Comparing base (9234de6) to head (bd27b50).
Report is 17 commits behind head on develop.

Files with missing lines Patch % Lines
...ibraries/push/impl/push/OnRedactedEventReceived.kt 37.14% 21 Missing and 1 partial ⚠️
...ifications/NotificationBroadcastReceiverHandler.kt 20.00% 4 Missing ⚠️
...sh/impl/notifications/NotificationResolverQueue.kt 87.09% 0 Missing and 4 partials ⚠️
...atrix/impl/notification/RustNotificationService.kt 81.25% 1 Missing and 2 partials ⚠️
.../libraries/push/impl/push/SyncOnNotifiableEvent.kt 80.00% 0 Missing and 3 partials ⚠️
.../notifications/DefaultNotificationDrawerManager.kt 0.00% 2 Missing ⚠️
...ications/DefaultOnMissedCallNotificationHandler.kt 0.00% 0 Missing and 2 partials ⚠️
...oid/libraries/push/impl/push/DefaultPushHandler.kt 96.22% 0 Missing and 2 partials ⚠️
...raries/push/impl/push/OnNotifiableEventReceived.kt 0.00% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #4722   +/-   ##
========================================
  Coverage    80.09%   80.10%           
========================================
  Files         2129     2130    +1     
  Lines        56416    56493   +77     
  Branches      7052     7070   +18     
========================================
+ Hits         45188    45254   +66     
- Misses        8811     8815    +4     
- Partials      2417     2424    +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for improving this part!
Some early remarks.


interface NotificationService {
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?>
suspend fun getNotification(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
suspend fun getNotifications(sessionId: SessionId, ids: Map<RoomId, List<EventId>>): Result<Map<EventId, NotificationData?>>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first sight, it's strange that the return type is not
Result<Map<RoomId, List<NotificationData>>>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was like that originally, but where it's immediately used later (here) having the notifications grouped by room id doesn't really help, we'd have to first search for the room, then use notifications.find { it.eventId == eventId }. Using having the id -> data mapping is more optimised.

suspend fun resolveEvents(
sessionId: SessionId,
notificationEventRequests: List<NotificationEventRequest>
): Result<Map<EventId, ResolvedPushEvent?>>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same type of remark, why don't we have a return type like:
Map<RoomId, List<ResolvedPushEvent>>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it might actually make sense to do that for the pushHistoryService.onSuccess and failure callbacks, searching might be a bit faster even if later we need to either iterate by room then event id or just flatten everything.

Maybe it would even make sense to use the NotificationEventRequest as the key itself? It has the session id, room and event ids to make fetching a single operation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmartinesp jmartinesp changed the title WIP: Notification batching Notification events resolving and rendering in batches May 16, 2025
@jmartinesp jmartinesp added the PR-Misc For other changes label May 16, 2025
@jmartinesp jmartinesp marked this pull request as ready for review May 16, 2025 08:23
@jmartinesp jmartinesp requested a review from a team as a code owner May 16, 2025 08:23
@jmartinesp jmartinesp requested review from bmarty and removed request for a team May 16, 2025 08:23
Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More comments. I will test the code on a real device.

@@ -30,8 +30,9 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
// Resolve the event and add a notification for it, at this point it should no longer be a ringing one
val notificationData = matrixClientProvider.getOrRestore(sessionId).getOrNull()
?.notificationService()
?.getNotification(roomId, eventId)
?.getNotifications(sessionId, mapOf(roomId to listOf(eventId)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks weird to have to provide a sessionId here, since we are using a method from a service of a matrix client.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but RustNotificationService doesn't have a reference to the matrix client. Maybe I should just add a SessionId as a constructor param for it. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should just add a SessionId as a constructor param for it. WDYT?

Yes please. We're doing it for RustRoomFactory already for instance.

Copy link
Member Author

@jmartinesp jmartinesp May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RustRoomFactory doesn't use injection though. Adding it here would mean creating a factory abstraction.

Never mind, I looked at the wrong component. I have a severe case of the Mondays, it seems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coroutineScope.launch {
while (coroutineScope.isActive) {
// Wait for a batch of requests to be received in a specified time window
delay(BATCH_WINDOW_MS.milliseconds)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this code will behave as a first throttler?
If we get a Push just before the delay is ended, it will trigger the request without waiting for any other potential push. I have not checked in practice how much time we have between mutliple pending push received from Firebase though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have to act as a throttle operation, but as a window one, grouping events that happen in an interval in batches. Those batches won't always be optimal, since as you say, if you receive one after one window closes it'll be processed on the next one.

If you think it's worth it implementing it more in a throttle/debounce way (without discarding items as those operators usually do) I can try to do that. There is the possibility that this means we chain lots of 'received item, wait for X ms' operations and end up delaying the processing for longer, but it's not really likely to happen in the real world except maybe for when you have several pushes in firebase that couldn't be delivered, the device reconnects and it starts receiving the pending pushes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bd27b50, it seems to work quite well.

val notificationManager = NotificationManagerCompat.from(context)
val previousNotification = notificationManager.activeNotifications.find { it.tag == roomInfo.roomId.value }
val elapsed = System.currentTimeMillis() - (previousNotification?.postTime ?: 0L)
Timber.d("Creating noisy notification for room ${roomInfo.roomId.value} with elapsed time $elapsed")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this log into the if block (not sure)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops, this should actually be gone. It was done for an experiment and I removed part of it, but apparently I kept this by mistake.

@jmartinesp jmartinesp requested a review from bmarty May 19, 2025 10:50
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR-Misc For other changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants