Skip to content

Conversation

@ouchadam
Copy link
Contributor

@ouchadam ouchadam commented Oct 13, 2021

Previously opened as #4185 and #4220

This entire change is the result of extracting, breaking down and unit testing 1 function NotificationDrawerManager.refreshNotificationDrawerBg

There are 3 main objectives/changes to the notification design

  1. The notification flow becomes a 2 pass system. All notifications are created and then all notifications are notified/cancelled, instead of looping through all the events notifying/cancelling as we go.
  • This has the benefit of reducing the time between posting individual message and summary group notification changes to the
    system UI message queue, which in turn should address our double notification problem 🤞 Doubled notifications #4152
  1. The queueEvents (previously called eventList) is the sole source of truth, we now simply render it as android notifications, when a notification is dismissed it triggers a delete action which causes the event to be reflected in the eventList, which in turn renders the newly missing event as a notification removal.
  • This avoids redundant notification cancel -> show flows, we only show or cancel event changes
  1. Testable, immutable and simpler to reason, we create all the notifications and then display all the notifications
  • Creates small classes with single responsibilities, tested where possible. The notification creation helpers RoomGroupMessageCreator & SummaryGroupMessageCreator are not particularly friendly to unit tests due to calling into android builders (although we could abstract that and test...)

  • Tentatively fixes the double notifications Doubled notifications #4152

  • Fixes notification related ANR [ANR] NotificationDrawerManager.setCurrentRoom  #4214

    • Splits the event list processing from the notification rendering, this allows the synchronised block to lock for less time (only whilst we process the events)
  • Fixes the serialised event list becoming out of sync with the currently rendered notifications, eg receiving multiple room invites

    • Adds event list and currently rendered events diffing, this allows us to remove out of sync notifications
BEFORE JONING ROOM EVENT AFTER JOINING ROOMS EVENT
before-accept-multiple after-multiple-joins
INCOMING MESSAGES ROOM INVITE ACCEPTING / READING
after-incoming-messages after-room-invite after-accepting-reading

override val isRedacted: Boolean = false
) : NotifiableEvent {

override var hasBeenDisplayed = false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

no longer needed as we diff the eventList and renderedEventsList

private val autoAcceptInvites: AutoAcceptInvites
) {

fun process(eventList: List<NotifiableEvent>, currentRoomId: String?, renderedEventsList: List<ProcessedEvent>): List<ProcessedEvent> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

a simplified version of https://github.com/vector-im/element-android/pull/4235/files#diff-c1e2253b1ef80b85a77d664922bf07c861d56dc472e8fbc51b8587ec264269daL255

  • no longer mutates the list which allows us to reduce the scope of the synchronised block
  • handles differences in the renderedEventsList and eventList keeping the notifications in sync


return bitmapLoader.getRoomBitmap(roomAvatarPath)
if (renderedEventsList == eventsToRender) {
Timber.d("Skipping notification update due to event list not changing")
Copy link
Contributor Author

@ouchadam ouchadam Oct 13, 2021

Choose a reason for hiding this comment

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

redundant notification updates can now avoid triggering any updates

private val notificationUtils: NotificationUtils
) {

fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

private val notificationUtils: NotificationUtils
) {

fun createSummaryNotification(roomNotifications: List<RoomNotification.Message.Meta>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

eventsToProcess: List<ProcessedEvent>) {
val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType()
with(notificationFactory) {
val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl)
Copy link
Contributor Author

@ouchadam ouchadam Oct 13, 2021

Choose a reason for hiding this comment

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

this is the main change of the PR

all the notifications are created and then displayed in a single pass, the aim being to reduce the time between the notification message queue being updated for the group and child notifications (hopefully eliminating the chance of the system displaying the group as a separate notification 🤞 )

@ouchadam ouchadam requested a review from bmarty October 13, 2021 10:56
}
val shouldUpdate = removeAll { it is NotifiableMessageEvent && it.roomId == roomId }
if (shouldUpdate) {
notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
Copy link
Contributor Author

@ouchadam ouchadam Oct 13, 2021

Choose a reason for hiding this comment

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

I have a sneaky suspicion this also might have been contributing to the duplicate notifications where we were eagerly cancelling whilst a refreshNotificationDrawerBg may have been running

Copy link
Member

Choose a reason for hiding this comment

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

good point

eventList.addAll(it.onlyKeptEvents())
}
// notice that we can get bit out of sync with actual display but not a big issue
firstTime = false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

firstTime is no longer needed, the diffing between renderedEventsList and eventList handles this for us

@github-actions
Copy link

github-actions bot commented Oct 13, 2021

Unit Test Results

  60 files    60 suites   58s ⏱️
115 tests 115 ✔️ 0 💤 0
340 runs  340 ✔️ 0 💤 0

Results for commit 0fe1b69.

♻️ This comment has been updated with latest results.

…e handled -

also puts in the basis for a separate notification refreshing implementation
- updates the logic to track when events are removed as a way for the notifications to remove themselves, null events mean they've been removed
- also handles when the event diff means the notifications should be removed
…nto a NotificationRender

- extract the displaying into its own class to avoid leaking the entire notificationutils
- cancel/display notification actions are completely driven by the event or abscense of event from the eventList
- attempts to avoid redundant render passes by checking if the eventList has changed since the last render
- the renderer's responsibility it handling events
…r source of truth

- when events have finished being displayed they should be removed from the eventList via notification delete actions
…l notifications

- adds some comments to explain the positioning
- this allows us to only synchronise of the event list modifications rather than the entire notification creation/rendering which should in turn reduce some of our ANRs #4214
…ed event id

- this state will be used to diff the currently rendered events against the new ones
… allow us to dismiss notifications from removed events
…cting the processed type when creating the notifications
@ouchadam ouchadam force-pushed the feature/adm/notification-logic-split branch from 799667f to 5029937 Compare October 14, 2021 15:58
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
wrapper.shortcutInfo?.let {
ShortcutManagerCompat.pushDynamicShortcut(appContext, it)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shortcut adding maintained, although now we're only doing it once per room rather than for every message in the room
https://github.com/vector-im/element-android/blob/develop/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt#L356

we're also able to easily remove the shortcuts when the notification is removed (if needed)

}

val largeBitmap = getRoomBitmap(events)
val shortcutInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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


private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
events.forEach { event ->
val senderPerson = if (event.outGoingMessage) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

This is an amazing work, thanks!
I have a few very minor remarks.
I did not run the code yet. Is it the last PR about your notification rework?

import timber.log.Timber
import javax.inject.Inject

class NotificationDisplayer @Inject constructor(context: Context) {
Copy link
Member

Choose a reason for hiding this comment

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

I like this simple class 👍

* we keep track of them in order to know which events have been removed from the eventList
* allowing us to cancel any notifications previous displayed by now removed events
*/
private var renderedEventsList = emptyList<Pair<ProcessedType, NotifiableEvent>>()
Copy link
Member

Choose a reason for hiding this comment

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

You could use the typealias ProcessedEvent here (or the data class :)).

}
val shouldUpdate = removeAll { it is NotifiableMessageEvent && it.roomId == roomId }
if (shouldUpdate) {
notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID)
Copy link
Member

Choose a reason for hiding this comment

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

good point

}
}

fun displayDiagnosticNotification() {
Copy link
Member

Choose a reason for hiding this comment

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

Was it not used here? I guess yes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

^^^ correct, unused


package im.vector.app.features.notifications

typealias ProcessedEvent = Pair<ProcessedType, NotifiableEvent>
Copy link
Member

Choose a reason for hiding this comment

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

I like typealias, but I do not like .first and .second, which are meaning less. I would prefer to have a simple data class in this case. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed, will switch to a data class 💯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done! f5de7f7

import androidx.core.content.pm.ShortcutInfoCompat
import javax.inject.Inject

private typealias ProcessedMessageEvent = Pair<ProcessedType, NotifiableMessageEvent>
Copy link
Member

Choose a reason for hiding this comment

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

same remark about typealias...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will replace with a data class 👍


private fun NotifiableMessageEvent.canNotBeDisplayed() = isRedacted

fun List<Pair<ProcessedType, InviteNotifiableEvent>>.toNotifications(myUserId: String): List<OneShotNotification> {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Maybe also use @JvmName() here, even if it's not strictly necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good idea 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also done as part of f5de7f7

@ouchadam
Copy link
Contributor Author

This is an amazing work, thanks! I have a few very minor remarks. I did not run the code yet. Is it the last PR about your notification rework?

thanks 😄 there's one more tiny PR around changing the notification ids to avoid 0 - 5 but this doesn't rely on the feature branch and can be done against develop

@ouchadam ouchadam force-pushed the feature/adm/notification-logic-split branch from f9e6718 to 34684cc Compare October 19, 2021 10:42
…assing around the process notificatiable events

- also includes @JvmName on all conflicting extensions for consistency
@ouchadam ouchadam force-pushed the feature/adm/notification-logic-split branch from 34684cc to f5de7f7 Compare October 19, 2021 10:43
) {

fun process(eventList: List<NotifiableEvent>, currentRoomId: String?, renderedEventsList: List<ProcessedEvent>): List<ProcessedEvent> {
fun process(eventList: List<NotifiableEvent>, currentRoomId: String?, renderedEventsList: ProcessedEvents): ProcessedEvents {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I know this is not new, but maybe the word List is not necessary in the parameter names. So instead of having renderedEventsList, we could simply have renderedEvents. The plural form is generally enough (to me) to understand that this is not a simple object. This is my point of view, I'm open to different ones :)

Copy link
Member

Choose a reason for hiding this comment

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

Also we can see some inconsistency in the naming convention between eventList and renderedEventsList :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will fix whilst I'm in the area 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bmarty
Copy link
Member

bmarty commented Oct 19, 2021

This is OK to me to merge this PR into the branch feature/adm/notification-redesign, but just to confirm, will you create a PR to merge feature/adm/notification-redesign into develop?
This will be my opportunity to test the change (I did not test it yet 🙈), and have a final (quick) review

@ouchadam
Copy link
Contributor Author

This is OK to me to merge this PR into the branch feature/adm/notification-redesign, but just to confirm, will you create a PR to merge feature/adm/notification-redesign into develop? This will be my opportunity to test the change (I did not test it yet see_no_evil), and have a final (quick) review

correct! I'll raise the entire feature branch as a PR against develop

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.

LGTM, please merge the PR whenever you want!

fun process(eventList: List<NotifiableEvent>, currentRoomId: String?, renderedEventsList: ProcessedEvents): ProcessedEvents {
val processedEventList = eventList.map {
fun process(queuedEvents: List<NotifiableEvent>, currentRoomId: String?, renderedEvents: ProcessedEvents): ProcessedEvents {
val processedEventList = queuedEvents.map {
Copy link
Member

Choose a reason for hiding this comment

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

Please do not change it, but there is still a List suffix in processedEventList (I know I am a pain sometimes 😆 )

Copy link
Contributor Author

@ouchadam ouchadam Oct 20, 2021

Choose a reason for hiding this comment

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

ah, missed that 👍

and I'm happy to fix it! will only take a few seconds (maybe longer for the CI 😅 )
0fe1b69

@ouchadam ouchadam force-pushed the feature/adm/notification-logic-split branch from 3e10145 to 0fe1b69 Compare October 20, 2021 10:07
@ouchadam ouchadam merged commit d8919e8 into feature/adm/notification-redesign Oct 20, 2021
@ouchadam ouchadam deleted the feature/adm/notification-logic-split branch October 20, 2021 10:36
@ouchadam ouchadam mentioned this pull request Oct 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants