Merge pull request #5645 from element-hq/feature/bma/mutliAccountNotification

Improve rendering notification for multi account
This commit is contained in:
Benoit Marty 2025-11-05 18:08:20 +01:00 committed by GitHub
commit 37f10ced4a
34 changed files with 464 additions and 433 deletions

View file

@ -93,6 +93,7 @@ dependencies {
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.matrixuiTest)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)

View file

@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_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.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest

View file

@ -33,9 +33,9 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder
import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
@ -415,6 +415,7 @@ class DefaultActiveCallManagerTest {
verify { notificationManagerCompat.cancel(any()) } verify { notificationManagerCompat.cancel(any()) }
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `IncomingCall - ignore expired ring lifetime`() = runTest { fun `IncomingCall - ignore expired ring lifetime`() = runTest {

View file

@ -24,11 +24,12 @@ class FakeEnterpriseService(
private val defaultHomeserverListResult: () -> List<String> = { emptyList() }, private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default, initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default,
initialBrandColor: Color? = null,
private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() }, private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService { ) : EnterpriseService {
private val brandColorState = MutableStateFlow<Color?>(null) private val brandColorState = MutableStateFlow(initialBrandColor)
private val semanticColorsState = MutableStateFlow(initialSemanticColors) private val semanticColorsState = MutableStateFlow(initialSemanticColors)
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask { override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.test package io.element.android.libraries.matrix.test
import androidx.annotation.ColorInt
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -99,4 +100,5 @@ const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM"
const val A_LOGIN_HINT = "mxid:@alice:example.org" const val A_LOGIN_HINT = "mxid:@alice:example.org"
const val A_COLOR_INT = 0xFF0000 @ColorInt
const val A_COLOR_INT: Int = 0xFFFF0000.toInt()

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.matrix.ui.test"
}
dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(libs.coil.compose)
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.test.media
import coil3.ComponentRegistry
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.request.Disposable
import coil3.request.ImageRequest
import coil3.request.ImageResult
class FakeImageLoader : ImageLoader {
private val executedRequests = mutableListOf<ImageRequest>()
override val defaults: ImageRequest.Defaults
get() = error("Not implemented")
override val components: ComponentRegistry
get() = error("Not implemented")
override val memoryCache: MemoryCache?
get() = error("Not implemented")
override val diskCache: DiskCache?
get() = error("Not implemented")
override fun enqueue(request: ImageRequest): Disposable {
error("Not implemented")
}
override suspend fun execute(request: ImageRequest): ImageResult {
executedRequests.add(request)
error("Not implemented")
}
override fun shutdown() {
error("Not implemented")
}
override fun newBuilder(): ImageLoader.Builder {
error("Not implemented")
}
fun getExecutedRequestsData(): List<Any> {
return executedRequests.map { it.data }
}
}

View file

@ -1,21 +1,22 @@
/* /*
* Copyright 2024 New Vector Ltd. * Copyright 2025 New Vector Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.libraries.push.test.notifications package io.element.android.libraries.matrix.ui.test.media
import coil3.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
class FakeImageLoaderHolder : ImageLoaderHolder { class FakeImageLoaderHolder(
private val fakeImageLoader = FakeImageLoader() val fakeImageLoader: ImageLoader = FakeImageLoader(),
) : ImageLoaderHolder {
override fun get(client: MatrixClient): ImageLoader { override fun get(client: MatrixClient): ImageLoader {
return fakeImageLoader.getImageLoader() return fakeImageLoader
} }
override fun remove(sessionId: SessionId) { override fun remove(sessionId: SessionId) {

View file

@ -76,6 +76,7 @@ dependencies {
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(libs.coil.test) testImplementation(libs.coil.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.matrixuiTest)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)

View file

@ -7,15 +7,10 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -32,11 +27,7 @@ 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.currentSessionId import io.element.android.services.appnavstate.api.currentSessionId
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag)
/** /**
* This class receives notification events as they arrive from the PushHandler calling [onNotifiableEventReceived] and * This class receives notification events as they arrive from the PushHandler calling [onNotifiableEventReceived] and
@ -46,7 +37,7 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultNotificationDrawerManager( class DefaultNotificationDrawerManager(
private val notificationManager: NotificationManagerCompat, private val notificationDisplayer: NotificationDisplayer,
private val notificationRenderer: NotificationRenderer, private val notificationRenderer: NotificationRenderer,
private val appNavigationStateService: AppNavigationStateService, private val appNavigationStateService: AppNavigationStateService,
@AppCoroutineScope @AppCoroutineScope
@ -55,25 +46,17 @@ class DefaultNotificationDrawerManager(
private val imageLoaderHolder: ImageLoaderHolder, private val imageLoaderHolder: ImageLoaderHolder,
private val activeNotificationsProvider: ActiveNotificationsProvider, private val activeNotificationsProvider: ActiveNotificationsProvider,
) : NotificationCleaner { ) : NotificationCleaner {
private var appNavigationStateObserver: Job? = null
// TODO EAx add a setting per user for this // TODO EAx add a setting per user for this
private var useCompleteNotificationFormat = true private var useCompleteNotificationFormat = true
init { init {
// Observe application state // Observe application state
appNavigationStateObserver = coroutineScope.launch { coroutineScope.launch {
appNavigationStateService.appNavigationState appNavigationStateService.appNavigationState
.collect { onAppNavigationStateChange(it.navigationState) } .collect { onAppNavigationStateChange(it.navigationState) }
} }
} }
// For test only
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun destroy() {
appNavigationStateObserver?.cancel()
}
private var currentAppNavigationState: NavigationState? = null private var currentAppNavigationState: NavigationState? = null
private fun onAppNavigationStateChange(navigationState: NavigationState) { private fun onAppNavigationStateChange(navigationState: NavigationState) {
@ -124,7 +107,7 @@ class DefaultNotificationDrawerManager(
* Clear all known message events for a [sessionId]. * Clear all known message events for a [sessionId].
*/ */
override fun clearAllMessagesEvents(sessionId: SessionId) { override fun clearAllMessagesEvents(sessionId: SessionId) {
notificationManager.cancel(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) notificationDisplayer.cancelNotification(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
clearSummaryNotificationIfNeeded(sessionId) clearSummaryNotificationIfNeeded(sessionId)
} }
@ -133,7 +116,7 @@ class DefaultNotificationDrawerManager(
*/ */
fun clearAllEvents(sessionId: SessionId) { fun clearAllEvents(sessionId: SessionId) {
activeNotificationsProvider.getNotificationsForSession(sessionId) activeNotificationsProvider.getNotificationsForSession(sessionId)
.forEach { notificationManager.cancel(it.tag, it.id) } .forEach { notificationDisplayer.cancelNotification(it.tag, it.id) }
} }
/** /**
@ -142,7 +125,7 @@ class DefaultNotificationDrawerManager(
* Can also be called when a notification for this room is dismissed by the user. * Can also be called when a notification for this room is dismissed by the user.
*/ */
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
notificationManager.cancel(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) notificationDisplayer.cancelNotification(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
clearSummaryNotificationIfNeeded(sessionId) clearSummaryNotificationIfNeeded(sessionId)
} }
@ -152,13 +135,13 @@ class DefaultNotificationDrawerManager(
*/ */
override fun clearMessagesForThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) { override fun clearMessagesForThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
val tag = NotificationCreator.messageTag(roomId, threadId) val tag = NotificationCreator.messageTag(roomId, threadId)
notificationManager.cancel(tag, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) notificationDisplayer.cancelNotification(tag, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
clearSummaryNotificationIfNeeded(sessionId) clearSummaryNotificationIfNeeded(sessionId)
} }
override fun clearMembershipNotificationForSession(sessionId: SessionId) { override fun clearMembershipNotificationForSession(sessionId: SessionId) {
activeNotificationsProvider.getMembershipNotificationForSession(sessionId) activeNotificationsProvider.getMembershipNotificationForSession(sessionId)
.forEach { notificationManager.cancel(it.tag, it.id) } .forEach { notificationDisplayer.cancelNotification(it.tag, it.id) }
clearSummaryNotificationIfNeeded(sessionId) clearSummaryNotificationIfNeeded(sessionId)
} }
@ -167,7 +150,7 @@ class DefaultNotificationDrawerManager(
*/ */
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) { override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
activeNotificationsProvider.getMembershipNotificationForRoom(sessionId, roomId) activeNotificationsProvider.getMembershipNotificationForRoom(sessionId, roomId)
.forEach { notificationManager.cancel(it.tag, it.id) } .forEach { notificationDisplayer.cancelNotification(it.tag, it.id) }
clearSummaryNotificationIfNeeded(sessionId) clearSummaryNotificationIfNeeded(sessionId)
} }
@ -176,14 +159,14 @@ class DefaultNotificationDrawerManager(
*/ */
override fun clearEvent(sessionId: SessionId, eventId: EventId) { override fun clearEvent(sessionId: SessionId, eventId: EventId) {
val id = NotificationIdProvider.getRoomEventNotificationId(sessionId) val id = NotificationIdProvider.getRoomEventNotificationId(sessionId)
notificationManager.cancel(eventId.value, id) notificationDisplayer.cancelNotification(eventId.value, id)
clearSummaryNotificationIfNeeded(sessionId) clearSummaryNotificationIfNeeded(sessionId)
} }
private fun clearSummaryNotificationIfNeeded(sessionId: SessionId) { private fun clearSummaryNotificationIfNeeded(sessionId: SessionId) {
val summaryNotification = activeNotificationsProvider.getSummaryNotification(sessionId) val summaryNotification = activeNotificationsProvider.getSummaryNotification(sessionId)
if (summaryNotification != null && activeNotificationsProvider.count(sessionId) == 1) { if (summaryNotification != null && activeNotificationsProvider.count(sessionId) == 1) {
notificationManager.cancel(null, summaryNotification.id) notificationDisplayer.cancelNotification(null, summaryNotification.id)
} }
} }
@ -201,29 +184,9 @@ class DefaultNotificationDrawerManager(
// We have an avatar and a display name, use it // We have an avatar and a display name, use it
userFromCache userFromCache
} else { } else {
client.getSafeUserProfile() client.getUserProfile().getOrNull() ?: MatrixUser(sessionId)
} }
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader) notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader)
} }
} }
private suspend fun MatrixClient.getSafeUserProfile(): MatrixUser {
return tryOrNull(
onException = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") },
operation = {
val profile = getUserProfile().getOrNull()
// displayName cannot be empty else NotificationCompat.MessagingStyle() will crash
if (profile?.displayName.isNullOrEmpty()) {
profile?.copy(displayName = sessionId.value)
} else {
profile
}
}
) ?: MatrixUser(
userId = sessionId,
displayName = sessionId.value,
avatarUrl = null
)
}
} }

View file

@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import android.graphics.Typeface import android.graphics.Typeface
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.annotation.ColorInt
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import coil3.ImageLoader import coil3.ImageLoader
@ -19,8 +18,8 @@ import dev.zacsweers.metro.ContributesBinding
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.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
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.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@ -31,39 +30,37 @@ import io.element.android.services.toolbox.api.strings.StringProvider
interface NotificationDataFactory { interface NotificationDataFactory {
suspend fun toNotifications( suspend fun toNotifications(
messages: List<NotifiableMessageEvent>, messages: List<NotifiableMessageEvent>,
currentUser: MatrixUser,
imageLoader: ImageLoader, imageLoader: ImageLoader,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<RoomNotification> ): List<RoomNotification>
@JvmName("toNotificationInvites") @JvmName("toNotificationInvites")
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications( fun toNotifications(
invites: List<InviteNotifiableEvent>, invites: List<InviteNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> ): List<OneShotNotification>
@JvmName("toNotificationSimpleEvents") @JvmName("toNotificationSimpleEvents")
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications( fun toNotifications(
simpleEvents: List<SimpleNotifiableEvent>, simpleEvents: List<SimpleNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> ): List<OneShotNotification>
@JvmName("toNotificationFallbackEvents") @JvmName("toNotificationFallbackEvents")
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications( fun toNotifications(
fallback: List<FallbackNotifiableEvent>, fallback: List<FallbackNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> ): List<OneShotNotification>
fun createSummaryNotification( fun createSummaryNotification(
currentUser: MatrixUser,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>, fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): SummaryNotification ): SummaryNotification
} }
@ -77,9 +74,8 @@ class DefaultNotificationDataFactory(
) : NotificationDataFactory { ) : NotificationDataFactory {
override suspend fun toNotifications( override suspend fun toNotifications(
messages: List<NotifiableMessageEvent>, messages: List<NotifiableMessageEvent>,
currentUser: MatrixUser,
imageLoader: ImageLoader, imageLoader: ImageLoader,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<RoomNotification> { ): List<RoomNotification> {
val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() } val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() }
.groupBy { it.roomId } .groupBy { it.roomId }
@ -90,13 +86,12 @@ class DefaultNotificationDataFactory(
eventsByThreadId.map { (threadId, events) -> eventsByThreadId.map { (threadId, events) ->
val notification = roomGroupMessageCreator.createRoomMessage( val notification = roomGroupMessageCreator.createRoomMessage(
currentUser = currentUser,
events = events, events = events,
roomId = roomId, roomId = roomId,
threadId = threadId, threadId = threadId,
imageLoader = imageLoader, imageLoader = imageLoader,
existingNotification = getExistingNotificationForMessages(currentUser.userId, roomId, threadId), existingNotification = getExistingNotificationForMessages(notificationAccountParams.user.userId, roomId, threadId),
color = color, notificationAccountParams = notificationAccountParams,
) )
RoomNotification( RoomNotification(
notification = notification, notification = notification,
@ -121,12 +116,12 @@ class DefaultNotificationDataFactory(
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications( override fun toNotifications(
invites: List<InviteNotifiableEvent>, invites: List<InviteNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> { ): List<OneShotNotification> {
return invites.map { event -> return invites.map { event ->
OneShotNotification( OneShotNotification(
key = event.roomId.value, tag = event.roomId.value,
notification = notificationCreator.createRoomInvitationNotification(event, color), notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event),
summaryLine = event.description, summaryLine = event.description,
isNoisy = event.noisy, isNoisy = event.noisy,
timestamp = event.timestamp timestamp = event.timestamp
@ -138,12 +133,12 @@ class DefaultNotificationDataFactory(
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications( override fun toNotifications(
simpleEvents: List<SimpleNotifiableEvent>, simpleEvents: List<SimpleNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> { ): List<OneShotNotification> {
return simpleEvents.map { event -> return simpleEvents.map { event ->
OneShotNotification( OneShotNotification(
key = event.eventId.value, tag = event.eventId.value,
notification = notificationCreator.createSimpleEventNotification(event, color), notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event),
summaryLine = event.description, summaryLine = event.description,
isNoisy = event.noisy, isNoisy = event.noisy,
timestamp = event.timestamp timestamp = event.timestamp
@ -155,12 +150,12 @@ class DefaultNotificationDataFactory(
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications( override fun toNotifications(
fallback: List<FallbackNotifiableEvent>, fallback: List<FallbackNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> { ): List<OneShotNotification> {
return fallback.map { event -> return fallback.map { event ->
OneShotNotification( OneShotNotification(
key = event.eventId.value, tag = event.eventId.value,
notification = notificationCreator.createFallbackNotification(event, color), notification = notificationCreator.createFallbackNotification(notificationAccountParams, event),
summaryLine = event.description.orEmpty(), summaryLine = event.description.orEmpty(),
isNoisy = false, isNoisy = false,
timestamp = event.timestamp timestamp = event.timestamp
@ -169,23 +164,21 @@ class DefaultNotificationDataFactory(
} }
override fun createSummaryNotification( override fun createSummaryNotification(
currentUser: MatrixUser,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>, fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): SummaryNotification { ): SummaryNotification {
return when { return when {
roomNotifications.isEmpty() && invitationNotifications.isEmpty() && simpleNotifications.isEmpty() -> SummaryNotification.Removed roomNotifications.isEmpty() && invitationNotifications.isEmpty() && simpleNotifications.isEmpty() -> SummaryNotification.Removed
else -> SummaryNotification.Update( else -> SummaryNotification.Update(
summaryGroupMessageCreator.createSummaryNotification( summaryGroupMessageCreator.createSummaryNotification(
currentUser = currentUser,
roomNotifications = roomNotifications, roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications, invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications, simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications, fallbackNotifications = fallbackNotifications,
color = color, notificationAccountParams = notificationAccountParams,
) )
) )
} }
@ -254,7 +247,7 @@ data class RoomNotification(
data class OneShotNotification( data class OneShotNotification(
val notification: Notification, val notification: Notification,
val key: String, val tag: String,
val summaryLine: CharSequence, val summaryLine: CharSequence,
val isNoisy: Boolean, val isNoisy: Boolean,
val timestamp: Long, val timestamp: Long,

View file

@ -19,8 +19,8 @@ import io.element.android.libraries.di.annotations.ApplicationContext
import timber.log.Timber import timber.log.Timber
interface NotificationDisplayer { interface NotificationDisplayer {
fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean fun showNotification(tag: String?, id: Int, notification: Notification): Boolean
fun cancelNotificationMessage(tag: String?, id: Int) fun cancelNotification(tag: String?, id: Int)
fun displayDiagnosticNotification(notification: Notification): Boolean fun displayDiagnosticNotification(notification: Notification): Boolean
fun dismissDiagnosticNotification() fun dismissDiagnosticNotification()
} }
@ -30,7 +30,7 @@ class DefaultNotificationDisplayer(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val notificationManager: NotificationManagerCompat private val notificationManager: NotificationManagerCompat
) : NotificationDisplayer { ) : NotificationDisplayer {
override fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean { override fun showNotification(tag: String?, id: Int, notification: Notification): Boolean {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
Timber.w("Not allowed to notify.") Timber.w("Not allowed to notify.")
return false return false
@ -40,26 +40,28 @@ class DefaultNotificationDisplayer(
return true return true
} }
override fun cancelNotificationMessage(tag: String?, id: Int) { override fun cancelNotification(tag: String?, id: Int) {
notificationManager.cancel(tag, id) notificationManager.cancel(tag, id)
} }
override fun displayDiagnosticNotification(notification: Notification): Boolean { override fun displayDiagnosticNotification(notification: Notification): Boolean {
return showNotificationMessage( return showNotification(
tag = "DIAGNOSTIC", tag = TAG_DIAGNOSTIC,
id = NOTIFICATION_ID_DIAGNOSTIC, id = NOTIFICATION_ID_DIAGNOSTIC,
notification = notification notification = notification
) )
} }
override fun dismissDiagnosticNotification() { override fun dismissDiagnosticNotification() {
cancelNotificationMessage( cancelNotification(
tag = "DIAGNOSTIC", tag = TAG_DIAGNOSTIC,
id = NOTIFICATION_ID_DIAGNOSTIC id = NOTIFICATION_ID_DIAGNOSTIC
) )
} }
companion object { companion object {
private const val TAG_DIAGNOSTIC = "DIAGNOSTIC"
/* ========================================================================================== /* ==========================================================================================
* IDs for notifications * IDs for notifications
* ========================================================================================== */ * ========================================================================================== */

View file

@ -15,6 +15,7 @@ import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
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.NotificationAccountParams
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.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@ -22,6 +23,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent 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.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import timber.log.Timber import timber.log.Timber
@ -32,6 +34,7 @@ class NotificationRenderer(
private val notificationDisplayer: NotificationDisplayer, private val notificationDisplayer: NotificationDisplayer,
private val notificationDataFactory: NotificationDataFactory, private val notificationDataFactory: NotificationDataFactory,
private val enterpriseService: EnterpriseService, private val enterpriseService: EnterpriseService,
private val sessionStore: SessionStore,
) { ) {
suspend fun render( suspend fun render(
currentUser: MatrixUser, currentUser: MatrixUser,
@ -41,24 +44,29 @@ class NotificationRenderer(
) { ) {
val color = enterpriseService.brandColorsFlow(currentUser.userId).first()?.toArgb() val color = enterpriseService.brandColorsFlow(currentUser.userId).first()?.toArgb()
?: NotificationConfig.NOTIFICATION_ACCENT_COLOR ?: NotificationConfig.NOTIFICATION_ACCENT_COLOR
val numberOfAccounts = sessionStore.numberOfSessions()
val notificationAccountParams = NotificationAccountParams(
user = currentUser,
color = color,
showSessionId = numberOfAccounts > 1,
)
val groupedEvents = eventsToProcess.groupByType() val groupedEvents = eventsToProcess.groupByType()
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, currentUser, imageLoader, color) val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, imageLoader, notificationAccountParams)
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, color) val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, notificationAccountParams)
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, color) val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, notificationAccountParams)
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, color) val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, notificationAccountParams)
val summaryNotification = notificationDataFactory.createSummaryNotification( val summaryNotification = notificationDataFactory.createSummaryNotification(
currentUser = currentUser,
roomNotifications = roomNotifications, roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications, invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications, simpleNotifications = simpleNotifications,
fallbackNotifications = fallbackNotifications, fallbackNotifications = fallbackNotifications,
color = color, notificationAccountParams = notificationAccountParams,
) )
// Remove summary first to avoid briefly displaying it after dismissing the last notification // Remove summary first to avoid briefly displaying it after dismissing the last notification
if (summaryNotification == SummaryNotification.Removed) { if (summaryNotification == SummaryNotification.Removed) {
Timber.tag(loggerTag.value).d("Removing summary notification") Timber.tag(loggerTag.value).d("Removing summary notification")
notificationDisplayer.cancelNotificationMessage( notificationDisplayer.cancelNotification(
tag = null, tag = null,
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId) id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
) )
@ -69,7 +77,7 @@ class NotificationRenderer(
roomId = notificationData.roomId, roomId = notificationData.roomId,
threadId = notificationData.threadId threadId = notificationData.threadId
) )
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotification(
tag = tag, tag = tag,
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId), id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
notification = notificationData.notification notification = notificationData.notification
@ -78,9 +86,9 @@ class NotificationRenderer(
invitationNotifications.forEach { notificationData -> invitationNotifications.forEach { notificationData ->
if (useCompleteNotificationFormat) { if (useCompleteNotificationFormat) {
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}") Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.tag}")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotification(
tag = notificationData.key, tag = notificationData.tag,
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
notification = notificationData.notification notification = notificationData.notification
) )
@ -89,9 +97,9 @@ class NotificationRenderer(
simpleNotifications.forEach { notificationData -> simpleNotifications.forEach { notificationData ->
if (useCompleteNotificationFormat) { if (useCompleteNotificationFormat) {
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}") Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.tag}")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotification(
tag = notificationData.key, tag = notificationData.tag,
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId), id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
notification = notificationData.notification notification = notificationData.notification
) )
@ -101,7 +109,7 @@ class NotificationRenderer(
// Show only the first fallback notification // Show only the first fallback notification
if (fallbackNotifications.isNotEmpty()) { if (fallbackNotifications.isNotEmpty()) {
Timber.tag(loggerTag.value).d("Showing fallback notification") Timber.tag(loggerTag.value).d("Showing fallback notification")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotification(
tag = "FALLBACK", tag = "FALLBACK",
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId), id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
notification = fallbackNotifications.first().notification notification = fallbackNotifications.first().notification
@ -111,7 +119,7 @@ class NotificationRenderer(
// Update summary last to avoid briefly displaying it before other notifications // Update summary last to avoid briefly displaying it before other notifications
if (summaryNotification is SummaryNotification.Update) { if (summaryNotification is SummaryNotification.Update) {
Timber.tag(loggerTag.value).d("Updating summary notification") Timber.tag(loggerTag.value).d("Updating summary notification")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotification(
tag = null, tag = null,
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId), id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
notification = summaryNotification.notification notification = summaryNotification.notification

View file

@ -9,15 +9,14 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.annotation.ColorInt
import coil3.ImageLoader import coil3.ImageLoader
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
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.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
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.factories.isSmartReplyError import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@ -25,13 +24,12 @@ import io.element.android.services.toolbox.api.strings.StringProvider
interface RoomGroupMessageCreator { interface RoomGroupMessageCreator {
suspend fun createRoomMessage( suspend fun createRoomMessage(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
roomId: RoomId, roomId: RoomId,
threadId: ThreadId?, threadId: ThreadId?,
imageLoader: ImageLoader, imageLoader: ImageLoader,
existingNotification: Notification?, existingNotification: Notification?,
@ColorInt color: Int,
): Notification ): Notification
} }
@ -42,13 +40,12 @@ class DefaultRoomGroupMessageCreator(
private val notificationCreator: NotificationCreator, private val notificationCreator: NotificationCreator,
) : RoomGroupMessageCreator { ) : RoomGroupMessageCreator {
override suspend fun createRoomMessage( override suspend fun createRoomMessage(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
roomId: RoomId, roomId: RoomId,
threadId: ThreadId?, threadId: ThreadId?,
imageLoader: ImageLoader, imageLoader: ImageLoader,
existingNotification: Notification?, existingNotification: Notification?,
@ColorInt color: Int,
): Notification { ): Notification {
val lastKnownRoomEvent = events.last() val lastKnownRoomEvent = events.last()
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderDisambiguatedDisplayName ?: "Room name (${roomId.value.take(8)}…)" val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderDisambiguatedDisplayName ?: "Room name (${roomId.value.take(8)}…)"
@ -66,8 +63,9 @@ class DefaultRoomGroupMessageCreator(
val smartReplyErrors = events.filter { it.isSmartReplyError() } val smartReplyErrors = events.filter { it.isSmartReplyError() }
val roomIsDm = !roomIsGroup val roomIsDm = !roomIsGroup
return notificationCreator.createMessagesListNotification( return notificationCreator.createMessagesListNotification(
notificationAccountParams = notificationAccountParams,
RoomEventGroupInfo( RoomEventGroupInfo(
sessionId = currentUser.userId, sessionId = notificationAccountParams.user.userId,
roomId = roomId, roomId = roomId,
roomDisplayName = roomName, roomDisplayName = roomName,
isDm = roomIsDm, isDm = roomIsDm,
@ -80,11 +78,9 @@ class DefaultRoomGroupMessageCreator(
largeIcon = largeBitmap, largeIcon = largeBitmap,
lastMessageTimestamp = lastMessageTimestamp, lastMessageTimestamp = lastMessageTimestamp,
tickerText = tickerText, tickerText = tickerText,
currentUser = currentUser,
existingNotification = existingNotification, existingNotification = existingNotification,
imageLoader = imageLoader, imageLoader = imageLoader,
events = events, events = events,
color = color,
) )
} }

View file

@ -8,22 +8,20 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import androidx.annotation.ColorInt
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
interface SummaryGroupMessageCreator { interface SummaryGroupMessageCreator {
fun createSummaryNotification( fun createSummaryNotification(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>, fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): Notification ): Notification
} }
@ -42,30 +40,25 @@ class DefaultSummaryGroupMessageCreator(
private val notificationCreator: NotificationCreator, private val notificationCreator: NotificationCreator,
) : SummaryGroupMessageCreator { ) : SummaryGroupMessageCreator {
override fun createSummaryNotification( override fun createSummaryNotification(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>, fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): Notification { ): Notification {
val summaryIsNoisy = roomNotifications.any { it.shouldBing } || val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
invitationNotifications.any { it.isNoisy } || invitationNotifications.any { it.isNoisy } ||
simpleNotifications.any { it.isNoisy } simpleNotifications.any { it.isNoisy }
val lastMessageTimestamp = roomNotifications.lastOrNull()?.latestTimestamp val lastMessageTimestamp = roomNotifications.lastOrNull()?.latestTimestamp
?: invitationNotifications.lastOrNull()?.timestamp ?: invitationNotifications.lastOrNull()?.timestamp
?: simpleNotifications.last().timestamp ?: simpleNotifications.last().timestamp
val nbEvents = roomNotifications.size + invitationNotifications.size + simpleNotifications.size
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms
val nbEvents = roomNotifications.size + simpleNotifications.size
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
return notificationCreator.createSummaryListNotification( return notificationCreator.createSummaryListNotification(
currentUser, notificationAccountParams = notificationAccountParams,
sumTitle, sumTitle,
noisy = summaryIsNoisy, noisy = summaryIsNoisy,
lastMessageTimestamp = lastMessageTimestamp, lastMessageTimestamp = lastMessageTimestamp,
color = color,
) )
} }
} }

View file

@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.notifications.factories
import androidx.annotation.ColorInt
import io.element.android.libraries.matrix.api.user.MatrixUser
data class NotificationAccountParams(
val user: MatrixUser,
@ColorInt val color: Int,
val showSessionId: Boolean,
)

View file

@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
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.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
@ -47,42 +48,40 @@ interface NotificationCreator {
* Create a notification for a Room. * Create a notification for a Room.
*/ */
suspend fun createMessagesListNotification( suspend fun createMessagesListNotification(
notificationAccountParams: NotificationAccountParams,
roomInfo: RoomEventGroupInfo, roomInfo: RoomEventGroupInfo,
threadId: ThreadId?, threadId: ThreadId?,
largeIcon: Bitmap?, largeIcon: Bitmap?,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
tickerText: String, tickerText: String,
currentUser: MatrixUser,
existingNotification: Notification?, existingNotification: Notification?,
imageLoader: ImageLoader, imageLoader: ImageLoader,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
@ColorInt color: Int,
): Notification ): Notification
fun createRoomInvitationNotification( fun createRoomInvitationNotification(
notificationAccountParams: NotificationAccountParams,
inviteNotifiableEvent: InviteNotifiableEvent, inviteNotifiableEvent: InviteNotifiableEvent,
@ColorInt color: Int,
): Notification ): Notification
fun createSimpleEventNotification( fun createSimpleEventNotification(
notificationAccountParams: NotificationAccountParams,
simpleNotifiableEvent: SimpleNotifiableEvent, simpleNotifiableEvent: SimpleNotifiableEvent,
@ColorInt color: Int,
): Notification ): Notification
fun createFallbackNotification( fun createFallbackNotification(
notificationAccountParams: NotificationAccountParams,
fallbackNotifiableEvent: FallbackNotifiableEvent, fallbackNotifiableEvent: FallbackNotifiableEvent,
@ColorInt color: Int,
): Notification ): Notification
/** /**
* Create the summary notification. * Create the summary notification.
*/ */
fun createSummaryListNotification( fun createSummaryListNotification(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
@ColorInt color: Int,
): Notification ): Notification
fun createDiagnosticNotification( fun createDiagnosticNotification(
@ -118,16 +117,15 @@ class DefaultNotificationCreator(
* Create a notification for a Room. * Create a notification for a Room.
*/ */
override suspend fun createMessagesListNotification( override suspend fun createMessagesListNotification(
notificationAccountParams: NotificationAccountParams,
roomInfo: RoomEventGroupInfo, roomInfo: RoomEventGroupInfo,
threadId: ThreadId?, threadId: ThreadId?,
largeIcon: Bitmap?, largeIcon: Bitmap?,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
tickerText: String, tickerText: String,
currentUser: MatrixUser,
existingNotification: Notification?, existingNotification: Notification?,
imageLoader: ImageLoader, imageLoader: ImageLoader,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
@ColorInt color: Int,
): Notification { ): Notification {
// Build the pending intent for when the notification is clicked // Build the pending intent for when the notification is clicked
val eventId = events.firstOrNull()?.eventId val eventId = events.firstOrNull()?.eventId
@ -135,7 +133,6 @@ class DefaultNotificationCreator(
threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo.sessionId, roomInfo.roomId, eventId, threadId) threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo.sessionId, roomInfo.roomId, eventId, threadId)
else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId, eventId) else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId, eventId)
} }
val smallIcon = CommonDrawables.ic_notification
val containsMissedCall = events.any { it.type == EventType.RTC_NOTIFICATION } val containsMissedCall = events.any { it.type == EventType.RTC_NOTIFICATION }
val channelId = if (containsMissedCall) { val channelId = if (containsMissedCall) {
notificationChannels.getChannelForIncomingCall(false) notificationChannels.getChannelForIncomingCall(false)
@ -159,9 +156,6 @@ class DefaultNotificationCreator(
setShortcutId(createShortcutId(roomInfo.sessionId, roomInfo.roomId)) setShortcutId(createShortcutId(roomInfo.sessionId, roomInfo.roomId))
} }
} }
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID
.setGroup(roomInfo.sessionId.value)
.setGroupSummary(false) .setGroupSummary(false)
// In order to avoid notification making sound twice (due to the summary notification) // In order to avoid notification making sound twice (due to the summary notification)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
@ -171,8 +165,8 @@ class DefaultNotificationCreator(
val messagingStyle = existingNotification?.let { val messagingStyle = existingNotification?.let {
MessagingStyle.extractMessagingStyleFromNotification(it) MessagingStyle.extractMessagingStyleFromNotification(it)
} ?: messagingStyleFromCurrentUser( } ?: createMessagingStyleFromCurrentUser(
user = currentUser, user = notificationAccountParams.user,
imageLoader = imageLoader, imageLoader = imageLoader,
roomName = roomInfo.roomDisplayName, roomName = roomInfo.roomDisplayName,
isThread = threadId != null, isThread = threadId != null,
@ -187,9 +181,7 @@ class DefaultNotificationCreator(
.setWhen(lastMessageTimestamp) .setWhen(lastMessageTimestamp)
// MESSAGING_STYLE sets title and content for API 16 and above devices. // MESSAGING_STYLE sets title and content for API 16 and above devices.
.setStyle(messagingStyle) .setStyle(messagingStyle)
.setSmallIcon(smallIcon) .configureWith(notificationAccountParams)
// Set primary color (important for Wear 2.0 Notifications).
.setColor(color)
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for // Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
// 'importance' which is set in the NotificationChannel. The integers representing // 'importance' which is set in the NotificationChannel. The integers representing
// 'priority' are different from 'importance', so make sure you don't mix them. // 'priority' are different from 'importance', so make sure you don't mix them.
@ -202,7 +194,7 @@ class DefaultNotificationCreator(
setSound(it) setSound(it)
} }
*/ */
setLights(color, 500, 500) setLights(notificationAccountParams.color, 500, 500)
} else { } else {
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
@ -234,19 +226,16 @@ class DefaultNotificationCreator(
} }
override fun createRoomInvitationNotification( override fun createRoomInvitationNotification(
notificationAccountParams: NotificationAccountParams,
inviteNotifiableEvent: InviteNotifiableEvent, inviteNotifiableEvent: InviteNotifiableEvent,
@ColorInt color: Int,
): Notification { ): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5)) .setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5))
.setContentText(inviteNotifiableEvent.description.annotateForDebug(6)) .setContentText(inviteNotifiableEvent.description.annotateForDebug(6))
.setGroup(inviteNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .configureWith(notificationAccountParams)
.setColor(color)
.apply { .apply {
addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent)) addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent)) addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
@ -261,7 +250,7 @@ class DefaultNotificationCreator(
setSound(it) setSound(it)
} }
*/ */
setLights(color, 500, 500) setLights(notificationAccountParams.color, 500, 500)
} else { } else {
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
@ -277,19 +266,16 @@ class DefaultNotificationCreator(
} }
override fun createSimpleEventNotification( override fun createSimpleEventNotification(
notificationAccountParams: NotificationAccountParams,
simpleNotifiableEvent: SimpleNotifiableEvent, simpleNotifiableEvent: SimpleNotifiableEvent,
@ColorInt color: Int,
): Notification { ): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentTitle(buildMeta.applicationName.annotateForDebug(7)) .setContentTitle(buildMeta.applicationName.annotateForDebug(7))
.setContentText(simpleNotifiableEvent.description.annotateForDebug(8)) .setContentText(simpleNotifiableEvent.description.annotateForDebug(8))
.setGroup(simpleNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .configureWith(notificationAccountParams)
.setColor(color)
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId, null)) .setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId, null))
.apply { .apply {
@ -301,7 +287,7 @@ class DefaultNotificationCreator(
setSound(it) setSound(it)
} }
*/ */
setLights(color, 500, 500) setLights(notificationAccountParams.color, 500, 500)
} else { } else {
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
@ -310,19 +296,16 @@ class DefaultNotificationCreator(
} }
override fun createFallbackNotification( override fun createFallbackNotification(
notificationAccountParams: NotificationAccountParams,
fallbackNotifiableEvent: FallbackNotifiableEvent, fallbackNotifiableEvent: FallbackNotifiableEvent,
@ColorInt color: Int,
): Notification { ): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(false) val channelId = notificationChannels.getChannelIdForMessage(false)
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentTitle(buildMeta.applicationName.annotateForDebug(7)) .setContentTitle(buildMeta.applicationName.annotateForDebug(7))
.setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8)) .setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8))
.setGroup(fallbackNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .configureWith(notificationAccountParams)
.setColor(color)
.setAutoCancel(true) .setAutoCancel(true)
.setWhen(fallbackNotifiableEvent.timestamp) .setWhen(fallbackNotifiableEvent.timestamp)
// Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite // Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite
@ -343,24 +326,21 @@ class DefaultNotificationCreator(
* Create the summary notification. * Create the summary notification.
*/ */
override fun createSummaryListNotification( override fun createSummaryListNotification(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
@ColorInt color: Int,
): Notification { ): Notification {
val smallIcon = CommonDrawables.ic_notification
val channelId = notificationChannels.getChannelIdForMessage(noisy) val channelId = notificationChannels.getChannelIdForMessage(noisy)
val userId = notificationAccountParams.user.userId
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
// used in compat < N, after summary is built based on child notifications // used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp) .setWhen(lastMessageTimestamp)
.setCategory(NotificationCompat.CATEGORY_MESSAGE) .setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon)
.setGroup(currentUser.userId.value)
// set this notification as the summary for the group // set this notification as the summary for the group
.setGroupSummary(true) .setGroupSummary(true)
.setColor(color) .configureWith(notificationAccountParams)
.apply { .apply {
if (noisy) { if (noisy) {
// Compat // Compat
@ -370,14 +350,14 @@ class DefaultNotificationCreator(
setSound(it) setSound(it)
} }
*/ */
setLights(color, 500, 500) setLights(notificationAccountParams.color, 500, 500)
} else { } else {
// compat // compat
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
} }
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId)) .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(userId))
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId)) .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(userId))
.build() .build()
} }
@ -468,7 +448,7 @@ class DefaultNotificationCreator(
} }
} }
private suspend fun messagingStyleFromCurrentUser( private suspend fun createMessagingStyleFromCurrentUser(
user: MatrixUser, user: MatrixUser,
imageLoader: ImageLoader, imageLoader: ImageLoader,
roomName: String, roomName: String,
@ -477,7 +457,8 @@ class DefaultNotificationCreator(
): MessagingStyle { ): MessagingStyle {
return MessagingStyle( return MessagingStyle(
Person.Builder() Person.Builder()
.setName(user.displayName?.annotateForDebug(50)) // Note: name cannot be empty else NotificationCompat.MessagingStyle() will crash
.setName(user.getBestName().annotateForDebug(50))
.setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader)) .setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader))
.setKey(user.userId.value) .setKey(user.userId.value)
.build() .build()
@ -497,4 +478,13 @@ class DefaultNotificationCreator(
} }
} }
private fun NotificationCompat.Builder.configureWith(notificationAccountParams: NotificationAccountParams) = apply {
setSmallIcon(CommonDrawables.ic_notification)
setColor(notificationAccountParams.color)
setGroup(notificationAccountParams.user.userId.value)
if (notificationAccountParams.showSessionId) {
setSubText(notificationAccountParams.user.userId.value)
}
}
fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed

View file

@ -73,7 +73,7 @@ class DefaultOnRedactedEventReceived(
oldMessage.person oldMessage.person
) )
messagingStyle.messages[messageToRedactIndex] = newMessage messagingStyle.messages[messageToRedactIndex] = newMessage
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotification(
statusBarNotification.tag, statusBarNotification.tag,
statusBarNotification.id, statusBarNotification.id,
NotificationCompat.Builder(context, notification) NotificationCompat.Builder(context, notification)

View file

@ -13,17 +13,17 @@ import androidx.core.app.NotificationCompat
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.NotificationConfig import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.test.A_COLOR_INT
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_TIMESTAMP import io.element.android.libraries.matrix.test.A_TIMESTAMP
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE
import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE
import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams
import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator
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.test.notifications.FakeImageLoader
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
@ -44,23 +44,22 @@ class DefaultBaseRoomGroupMessageCreatorTest {
val sut = createRoomGroupMessageCreator() val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage( val result = sut.createRoomMessage(
currentUser = aMatrixUser(), notificationAccountParams = aNotificationAccountParams(),
events = listOf( events = listOf(
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
imageUriString = "aUri", imageUriString = "aUri",
) )
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(), imageLoader = fakeImageLoader,
existingNotification = null, existingNotification = null,
threadId = null, threadId = null,
color = A_COLOR_INT,
) )
assertThat(result.number).isEqualTo(1) assertThat(result.number).isEqualTo(1)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_LOW) assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_LOW)
assertThat(result.`when`).isEqualTo(A_TIMESTAMP) assertThat(result.`when`).isEqualTo(A_TIMESTAMP)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
@Test @Test
@ -68,21 +67,20 @@ class DefaultBaseRoomGroupMessageCreatorTest {
val sut = createRoomGroupMessageCreator() val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage( val result = sut.createRoomMessage(
currentUser = aMatrixUser(), notificationAccountParams = aNotificationAccountParams(),
events = listOf( events = listOf(
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
noisy = true, noisy = true,
) )
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(), imageLoader = fakeImageLoader,
existingNotification = null, existingNotification = null,
threadId = null, threadId = null,
color = A_COLOR_INT,
) )
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT) assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
@Test @Test
@ -130,9 +128,11 @@ class DefaultBaseRoomGroupMessageCreatorTest {
sdkIntProvider = FakeBuildVersionSdkIntProvider(api) sdkIntProvider = FakeBuildVersionSdkIntProvider(api)
) )
val result = sut.createRoomMessage( val result = sut.createRoomMessage(
currentUser = aMatrixUser( notificationAccountParams = aNotificationAccountParams(
// Some user avatar user = aMatrixUser(
avatarUrl = A_USER_AVATAR_1, // Some user avatar
avatarUrl = A_USER_AVATAR_1,
)
), ),
events = listOf( events = listOf(
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
@ -141,13 +141,12 @@ class DefaultBaseRoomGroupMessageCreatorTest {
) )
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(), imageLoader = fakeImageLoader,
existingNotification = null, existingNotification = null,
threadId = null, threadId = null,
color = A_COLOR_INT,
) )
assertThat(result.number).isEqualTo(1) assertThat(result.number).isEqualTo(1)
assertThat(fakeImageLoader.getCoilRequests()).containsExactlyElementsIn(expectedCoilRequests) assertThat(fakeImageLoader.getExecutedRequestsData()).containsExactlyElementsIn(expectedCoilRequests)
} }
@Test @Test
@ -155,16 +154,15 @@ class DefaultBaseRoomGroupMessageCreatorTest {
val sut = createRoomGroupMessageCreator() val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage( val result = sut.createRoomMessage(
currentUser = aMatrixUser(), notificationAccountParams = aNotificationAccountParams(),
events = listOf( events = listOf(
aNotifiableMessageEvent(timestamp = A_TIMESTAMP), aNotifiableMessageEvent(timestamp = A_TIMESTAMP),
aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10),
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(), imageLoader = fakeImageLoader,
existingNotification = null, existingNotification = null,
threadId = null, threadId = null,
color = A_COLOR_INT,
) )
assertThat(result.number).isEqualTo(2) assertThat(result.number).isEqualTo(2)
assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10) assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10)
@ -175,7 +173,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
QUICK_REPLY_ACTION_TITLE.takeIf { NotificationConfig.SHOW_QUICK_REPLY_ACTION }, QUICK_REPLY_ACTION_TITLE.takeIf { NotificationConfig.SHOW_QUICK_REPLY_ACTION },
) )
) )
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
@Test @Test
@ -183,7 +181,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
val sut = createRoomGroupMessageCreator() val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage( val result = sut.createRoomMessage(
currentUser = aMatrixUser(), notificationAccountParams = aNotificationAccountParams(),
events = listOf( events = listOf(
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
outGoingMessage = true, outGoingMessage = true,
@ -191,10 +189,9 @@ class DefaultBaseRoomGroupMessageCreatorTest {
), ),
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(), imageLoader = fakeImageLoader,
existingNotification = null, existingNotification = null,
threadId = null, threadId = null,
color = A_COLOR_INT,
) )
val actionTitles = result.actions?.map { it.title } val actionTitles = result.actions?.map { it.title }
assertThat(actionTitles).isEqualTo( assertThat(actionTitles).isEqualTo(
@ -202,7 +199,7 @@ class DefaultBaseRoomGroupMessageCreatorTest {
MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION } MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION }
) )
) )
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
@Test @Test
@ -210,21 +207,20 @@ class DefaultBaseRoomGroupMessageCreatorTest {
val sut = createRoomGroupMessageCreator() val sut = createRoomGroupMessageCreator()
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = sut.createRoomMessage( val result = sut.createRoomMessage(
currentUser = aMatrixUser(), notificationAccountParams = aNotificationAccountParams(),
events = listOf( events = listOf(
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
roomIsDm = true, roomIsDm = true,
), ),
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
imageLoader = fakeImageLoader.getImageLoader(), imageLoader = fakeImageLoader,
existingNotification = null, existingNotification = null,
threadId = null, threadId = null,
color = A_COLOR_INT,
) )
assertThat(result.number).isEqualTo(1) assertThat(result.number).isEqualTo(1)
assertThat(result.`when`).isEqualTo(A_TIMESTAMP) assertThat(result.`when`).isEqualTo(A_TIMESTAMP)
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
} }

View file

@ -8,10 +8,10 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import androidx.core.app.NotificationManagerCompat import androidx.compose.ui.graphics.Color
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
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_AVATAR_URL
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_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -20,13 +20,17 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID
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
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder
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.aNotificationAccountParams
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
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.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
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.api.NavigationState
@ -38,25 +42,19 @@ 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 io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow 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
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class DefaultNotificationDrawerManagerTest { class DefaultNotificationDrawerManagerTest {
@Test @Test
fun `clearAllEvents should have no effect when queue is empty`() = runTest { fun `clearAllEvents should have no effect when queue is empty`() = runTest {
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager()
defaultNotificationDrawerManager.clearAllEvents(A_SESSION_ID) defaultNotificationDrawerManager.clearAllEvents(A_SESSION_ID)
defaultNotificationDrawerManager.destroy()
} }
@Test @Test
@ -64,8 +62,8 @@ class DefaultNotificationDrawerManagerTest {
// 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 matrixUser = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice", avatarUrl = "mxc://data") val matrixUser = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice", avatarUrl = "mxc://data")
val mockRoomGroupMessageCreator = FakeRoomGroupMessageCreator( val mockRoomGroupMessageCreator = FakeRoomGroupMessageCreator(
createRoomMessageResult = lambdaRecorder { user, _, roomId, _, _, existingNotification -> createRoomMessageResult = lambdaRecorder { notificationAccountParams, _, roomId, _, _, existingNotification ->
assertThat(user).isEqualTo(matrixUser) assertThat(notificationAccountParams.user).isEqualTo(matrixUser)
assertThat(roomId).isEqualTo(A_ROOM_ID) assertThat(roomId).isEqualTo(A_ROOM_ID)
assertThat(existingNotification).isNull() assertThat(existingNotification).isNull()
Notification() Notification()
@ -88,7 +86,6 @@ class DefaultNotificationDrawerManagerTest {
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
// Add the same Event again (will be ignored) // Add the same Event again (will be ignored)
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
defaultNotificationDrawerManager.destroy()
} }
@Test @Test
@ -101,7 +98,7 @@ class DefaultNotificationDrawerManagerTest {
) )
) )
val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow) val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow)
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( createDefaultNotificationDrawerManager(
appNavigationStateService = appNavigationStateService appNavigationStateService = appNavigationStateService
) )
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true))
@ -117,17 +114,22 @@ class DefaultNotificationDrawerManagerTest {
// Like a user sign out // Like a user sign out
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true))
runCurrent() runCurrent()
defaultNotificationDrawerManager.destroy()
} }
@Test @Test
fun `when MatrixClient has no cached user name a fallback one is used to render the notification`() = runTest { fun `when MatrixClient has no cached user name and avatar, the profile is loaded to render the notification`() = runTest {
val matrixClient = FakeMatrixClient(userDisplayName = null) val matrixClient = FakeMatrixClient(
userDisplayName = null,
userAvatarUrl = null,
)
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
val messageCreator = FakeRoomGroupMessageCreator() val messageCreator = FakeRoomGroupMessageCreator()
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
matrixClientProvider = matrixClientProvider, matrixClientProvider = matrixClientProvider,
roomGroupMessageCreator = messageCreator, roomGroupMessageCreator = messageCreator,
enterpriseService = FakeEnterpriseService(
initialBrandColor = Color.Red,
)
) )
// Gets a display name from MatrixClient.getUserProfile // Gets a display name from MatrixClient.getUserProfile
matrixClient.givenGetProfileResult(A_SESSION_ID, Result.success(aMatrixUser(id = A_SESSION_ID.value, displayName = "alice"))) matrixClient.givenGetProfileResult(A_SESSION_ID, Result.success(aMatrixUser(id = A_SESSION_ID.value, displayName = "alice")))
@ -144,27 +146,41 @@ class DefaultNotificationDrawerManagerTest {
messageCreator.createRoomMessageResult.assertions() messageCreator.createRoomMessageResult.assertions()
.isCalledExactly(3) .isCalledExactly(3)
.withSequence( .withSequence(
listOf(value(aMatrixUser(id = A_SESSION_ID.value, displayName = "alice")), any(), any(), any(), any(), any()),
listOf(value(aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value)), any(), any(), any(), any(), any()),
listOf( listOf(
value(aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value, avatarUrl = AN_AVATAR_URL)), value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice"))),
any(),
any(),
any(),
any(),
any(),
),
listOf(
value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = ""))),
any(),
any(),
any(),
any(),
any(),
),
listOf(
value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = null, avatarUrl = null))),
any(),
any(), any(),
any(), any(),
any(), any(),
any(), any(),
any()
), ),
) )
defaultNotificationDrawerManager.destroy()
} }
@Test @Test
fun `clearSummaryNotificationIfNeeded will run after clearing all other notifications`() = runTest { fun `clearSummaryNotificationIfNeeded will run after clearing all other notifications`() = runTest {
val notificationManager = mockk<NotificationManagerCompat> { val cancelNotificationResult = lambdaRecorder<String?, Int, Unit> { _, _ -> }
every { cancel(any(), any()) } returns Unit val notificationDisplayer = FakeNotificationDisplayer(
} cancelNotificationResult = cancelNotificationResult,
)
val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID) val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
val roomMessageId = NotificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID)
val activeNotificationsProvider = FakeActiveNotificationsProvider( val activeNotificationsProvider = FakeActiveNotificationsProvider(
getSummaryNotificationResult = { getSummaryNotificationResult = {
mockk { mockk {
@ -174,7 +190,7 @@ class DefaultNotificationDrawerManagerTest {
countResult = { 1 }, countResult = { 1 },
) )
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
notificationManager = notificationManager, notificationDisplayer = notificationDisplayer,
activeNotificationsProvider = activeNotificationsProvider, activeNotificationsProvider = activeNotificationsProvider,
) )
@ -182,24 +198,26 @@ class DefaultNotificationDrawerManagerTest {
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID) defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID)
// Verify we asked to cancel the notification with summaryId // Verify we asked to cancel the notification with summaryId
verify { notificationManager.cancel(null, summaryId) } cancelNotificationResult.assertions().isCalledExactly(2).withSequence(
listOf(value(null), value(roomMessageId)),
defaultNotificationDrawerManager.destroy() listOf(value(null), value(summaryId)),
)
} }
private fun TestScope.createDefaultNotificationDrawerManager( private fun TestScope.createDefaultNotificationDrawerManager(
notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(RuntimeEnvironment.getApplication()), notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(),
appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(), appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(),
roomGroupMessageCreator: RoomGroupMessageCreator = FakeRoomGroupMessageCreator(), roomGroupMessageCreator: RoomGroupMessageCreator = FakeRoomGroupMessageCreator(),
summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(), summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(),
activeNotificationsProvider: FakeActiveNotificationsProvider = FakeActiveNotificationsProvider(), activeNotificationsProvider: FakeActiveNotificationsProvider = FakeActiveNotificationsProvider(),
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
sessionStore: SessionStore = InMemorySessionStore(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
): DefaultNotificationDrawerManager { ): DefaultNotificationDrawerManager {
val context = RuntimeEnvironment.getApplication()
return DefaultNotificationDrawerManager( return DefaultNotificationDrawerManager(
notificationManager = notificationManager, notificationDisplayer = notificationDisplayer,
notificationRenderer = NotificationRenderer( notificationRenderer = NotificationRenderer(
notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)), notificationDisplayer = FakeNotificationDisplayer(),
notificationDataFactory = DefaultNotificationDataFactory( notificationDataFactory = DefaultNotificationDataFactory(
notificationCreator = FakeNotificationCreator(), notificationCreator = FakeNotificationCreator(),
roomGroupMessageCreator = roomGroupMessageCreator, roomGroupMessageCreator = roomGroupMessageCreator,
@ -207,10 +225,11 @@ class DefaultNotificationDrawerManagerTest {
activeNotificationsProvider = activeNotificationsProvider, activeNotificationsProvider = activeNotificationsProvider,
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
), ),
enterpriseService = FakeEnterpriseService(), enterpriseService = enterpriseService,
sessionStore = sessionStore,
), ),
appNavigationStateService = appNavigationStateService, appNavigationStateService = appNavigationStateService,
coroutineScope = this, coroutineScope = backgroundScope,
matrixClientProvider = matrixClientProvider, matrixClientProvider = matrixClientProvider,
imageLoaderHolder = FakeImageLoaderHolder(), imageLoaderHolder = FakeImageLoaderHolder(),
activeNotificationsProvider = activeNotificationsProvider, activeNotificationsProvider = activeNotificationsProvider,

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
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_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -16,23 +15,19 @@ 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.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.notification.aNotificationData import io.element.android.libraries.matrix.test.notification.aNotificationData
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory
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.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DefaultOnMissedCallNotificationHandlerTest { class DefaultOnMissedCallNotificationHandlerTest {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
@ -52,11 +47,9 @@ class DefaultOnMissedCallNotificationHandlerTest {
val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler( val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler(
matrixClientProvider = matrixClientProvider, matrixClientProvider = matrixClientProvider,
defaultNotificationDrawerManager = DefaultNotificationDrawerManager( defaultNotificationDrawerManager = DefaultNotificationDrawerManager(
notificationManager = mockk(relaxed = true), notificationDisplayer = FakeNotificationDisplayer(),
notificationRenderer = NotificationRenderer( notificationRenderer = createNotificationRenderer(
notificationDisplayer = FakeNotificationDisplayer(),
notificationDataFactory = dataFactory, notificationDataFactory = dataFactory,
enterpriseService = FakeEnterpriseService(),
), ),
appNavigationStateService = FakeAppNavigationStateService(), appNavigationStateService = FakeAppNavigationStateService(),
coroutineScope = backgroundScope, coroutineScope = backgroundScope,

View file

@ -10,9 +10,8 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_COLOR_INT
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.ui.components.aMatrixUser import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
@ -34,7 +33,7 @@ class DefaultSummaryGroupMessageCreatorTest {
) )
val result = summaryCreator.createSummaryNotification( val result = summaryCreator.createSummaryNotification(
currentUser = aMatrixUser(), notificationAccountParams = aNotificationAccountParams(),
roomNotifications = listOf( roomNotifications = listOf(
RoomNotification( RoomNotification(
notification = Notification(), notification = Notification(),
@ -49,12 +48,11 @@ class DefaultSummaryGroupMessageCreatorTest {
invitationNotifications = emptyList(), invitationNotifications = emptyList(),
simpleNotifications = emptyList(), simpleNotifications = emptyList(),
fallbackNotifications = emptyList(), fallbackNotifications = emptyList(),
color = A_COLOR_INT,
) )
notificationCreator.createSummaryListNotificationResult.assertions() notificationCreator.createSummaryListNotificationResult.assertions()
.isCalledOnce() .isCalledOnce()
.with(any(), nonNull(), any(), any()) .with(any(), any(), nonNull(), any(), any())
// Set from the events included // Set from the events included
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View file

@ -11,9 +11,10 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
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_COLOR_INT
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_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
@ -21,7 +22,6 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou
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.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import io.element.android.libraries.push.test.notifications.FakeImageLoader
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -51,16 +51,18 @@ class NotificationDataFactoryTest {
@Test @Test
fun `given a room invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) { fun `given a room invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) {
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT) val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(
aNotificationAccountParams(),
AN_INVITATION_EVENT,
)
val roomInvitation = listOf(AN_INVITATION_EVENT) val roomInvitation = listOf(AN_INVITATION_EVENT)
val result = toNotifications(roomInvitation, aNotificationAccountParams())
val result = toNotifications(roomInvitation, A_COLOR_INT)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
listOf( listOf(
OneShotNotification( OneShotNotification(
notification = expectedNotification, notification = expectedNotification,
key = A_ROOM_ID.value, tag = A_ROOM_ID.value,
summaryLine = AN_INVITATION_EVENT.description, summaryLine = AN_INVITATION_EVENT.description,
isNoisy = AN_INVITATION_EVENT.noisy, isNoisy = AN_INVITATION_EVENT.noisy,
timestamp = AN_INVITATION_EVENT.timestamp timestamp = AN_INVITATION_EVENT.timestamp
@ -71,20 +73,18 @@ class NotificationDataFactoryTest {
@Test @Test
fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) { fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) {
val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT) val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(
val roomInvitation = listOf(A_SIMPLE_EVENT) aNotificationAccountParams(),
AN_INVITATION_EVENT,
val result = toNotifications(roomInvitation, A_COLOR_INT) )
val result = toNotifications(listOf(A_SIMPLE_EVENT), aNotificationAccountParams())
assertThat(result).isEqualTo( assertThat(result).containsExactly(
listOf( OneShotNotification(
OneShotNotification( notification = expectedNotification,
notification = expectedNotification, tag = AN_EVENT_ID.value,
key = AN_EVENT_ID.value, summaryLine = A_SIMPLE_EVENT.description,
summaryLine = A_SIMPLE_EVENT.description, isNoisy = A_SIMPLE_EVENT.noisy,
isNoisy = A_SIMPLE_EVENT.noisy, timestamp = AN_INVITATION_EVENT.timestamp
timestamp = AN_INVITATION_EVENT.timestamp
)
) )
) )
} }
@ -94,13 +94,14 @@ class NotificationDataFactoryTest {
val events = listOf(A_MESSAGE_EVENT) val events = listOf(A_MESSAGE_EVENT)
val expectedNotification = RoomNotification( val expectedNotification = RoomNotification(
notification = fakeRoomGroupMessageCreator.createRoomMessage( notification = fakeRoomGroupMessageCreator.createRoomMessage(
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), notificationAccountParams = aNotificationAccountParams(
user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
),
events = events, events = events,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
threadId = null, threadId = null,
imageLoader = FakeImageLoader().getImageLoader(), imageLoader = FakeImageLoader(),
existingNotification = null, existingNotification = null,
color = A_COLOR_INT,
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
summaryLine = "A room name: Bob Hello world!", summaryLine = "A room name: Bob Hello world!",
@ -109,35 +110,33 @@ class NotificationDataFactoryTest {
shouldBing = events.any { it.noisy }, shouldBing = events.any { it.noisy },
threadId = null, threadId = null,
) )
val roomWithMessage = listOf(A_MESSAGE_EVENT)
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = toNotifications( val result = toNotifications(
messages = roomWithMessage, messages = listOf(A_MESSAGE_EVENT),
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), notificationAccountParams = aNotificationAccountParams(
imageLoader = fakeImageLoader.getImageLoader(), user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
color = A_COLOR_INT, ),
imageLoader = fakeImageLoader,
) )
assertThat(result.size).isEqualTo(1) assertThat(result.size).isEqualTo(1)
assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue() assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue()
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
@Test @Test
fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationDataFactory) { fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationDataFactory) {
val redactedRoom = listOf(A_MESSAGE_EVENT.copy(isRedacted = true)) val redactedRoom = A_MESSAGE_EVENT.copy(isRedacted = true)
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = toNotifications( val result = toNotifications(
messages = redactedRoom, messages = listOf(redactedRoom),
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), notificationAccountParams = aNotificationAccountParams(
imageLoader = fakeImageLoader.getImageLoader(), user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
color = A_COLOR_INT, ),
imageLoader = fakeImageLoader,
) )
assertThat(result).isEmpty() assertThat(result).isEmpty()
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
@Test @Test
@ -151,13 +150,14 @@ class NotificationDataFactoryTest {
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted"))) val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
val expectedNotification = RoomNotification( val expectedNotification = RoomNotification(
notification = fakeRoomGroupMessageCreator.createRoomMessage( notification = fakeRoomGroupMessageCreator.createRoomMessage(
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), notificationAccountParams = aNotificationAccountParams(
user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
),
events = withRedactedRemoved, events = withRedactedRemoved,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
threadId = null, threadId = null,
imageLoader = FakeImageLoader().getImageLoader(), imageLoader = FakeImageLoader(),
existingNotification = null, existingNotification = null,
color = A_COLOR_INT,
), ),
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
summaryLine = "A room name: Bob Hello world!", summaryLine = "A room name: Bob Hello world!",
@ -170,14 +170,15 @@ class NotificationDataFactoryTest {
val fakeImageLoader = FakeImageLoader() val fakeImageLoader = FakeImageLoader()
val result = toNotifications( val result = toNotifications(
messages = roomWithRedactedMessage, messages = roomWithRedactedMessage,
currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), notificationAccountParams = aNotificationAccountParams(
imageLoader = fakeImageLoader.getImageLoader(), user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
color = A_COLOR_INT, ),
imageLoader = fakeImageLoader,
) )
assertThat(result.size).isEqualTo(1) assertThat(result.size).isEqualTo(1)
assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue() assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue()
assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty()
} }
} }

View file

@ -7,14 +7,17 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
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.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
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_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader
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.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory
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
@ -23,7 +26,8 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent 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.fixtures.anInviteNotifiableEvent
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.test.notifications.FakeImageLoader import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
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
@ -38,7 +42,7 @@ private const val USE_COMPLETE_NOTIFICATION_FORMAT = true
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION) private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION)
private val ONE_SHOT_NOTIFICATION = private val ONE_SHOT_NOTIFICATION =
OneShotNotification(notification = A_NOTIFICATION, key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1) OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class NotificationRendererTest { class NotificationRendererTest {
@ -56,10 +60,9 @@ class NotificationRendererTest {
) )
private val notificationIdProvider = NotificationIdProvider private val notificationIdProvider = NotificationIdProvider
private val notificationRenderer = NotificationRenderer( private val notificationRenderer = createNotificationRenderer(
notificationDisplayer = notificationDisplayer, notificationDisplayer = notificationDisplayer,
notificationDataFactory = notificationDataFactory, notificationDataFactory = notificationDataFactory,
enterpriseService = FakeEnterpriseService(),
) )
@Test @Test
@ -75,7 +78,7 @@ class NotificationRendererTest {
renderEventsAsNotifications(listOf(aNotifiableMessageEvent())) renderEventsAsNotifications(listOf(aNotifiableMessageEvent()))
notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence( notificationDisplayer.showNotificationResult.assertions().isCalledExactly(2).withSequence(
listOf(value(A_ROOM_ID.value), value(notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)), listOf(value(A_ROOM_ID.value), value(notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)),
listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification)) listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification))
) )
@ -83,11 +86,11 @@ class NotificationRendererTest {
@Test @Test
fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest { fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest {
notificationCreator.createSimpleNotificationResult = lambdaRecorder { _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } notificationCreator.createSimpleNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(tag = AN_EVENT_ID.value).notification }
renderEventsAsNotifications(listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID))) renderEventsAsNotifications(listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID)))
notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence( notificationDisplayer.showNotificationResult.assertions().isCalledExactly(2).withSequence(
listOf(value(AN_EVENT_ID.value), value(notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)), listOf(value(AN_EVENT_ID.value), value(notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)),
listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification)) listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification))
) )
@ -95,11 +98,11 @@ class NotificationRendererTest {
@Test @Test
fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest { fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest {
notificationCreator.createRoomInvitationNotificationResult = lambdaRecorder { _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } notificationCreator.createRoomInvitationNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(tag = AN_EVENT_ID.value).notification }
renderEventsAsNotifications(listOf(anInviteNotifiableEvent())) renderEventsAsNotifications(listOf(anInviteNotifiableEvent()))
notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence( notificationDisplayer.showNotificationResult.assertions().isCalledExactly(2).withSequence(
listOf(value(A_ROOM_ID.value), value(notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)), listOf(value(A_ROOM_ID.value), value(notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)),
listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification)) listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification))
) )
@ -110,7 +113,19 @@ class NotificationRendererTest {
MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
eventsToProcess = events, eventsToProcess = events,
imageLoader = FakeImageLoader().getImageLoader(), imageLoader = FakeImageLoader(),
) )
} }
} }
fun createNotificationRenderer(
notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(),
notificationDataFactory: NotificationDataFactory = FakeNotificationDataFactory(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
sessionStore: SessionStore = InMemorySessionStore(),
) = NotificationRenderer(
notificationDisplayer = notificationDisplayer,
notificationDataFactory = notificationDataFactory,
enterpriseService = enterpriseService,
sessionStore = sessionStore,
)

View file

@ -20,9 +20,9 @@ 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_SESSION_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder
import io.element.android.libraries.push.impl.notifications.factories.FakeIntentProvider import io.element.android.libraries.push.impl.notifications.factories.FakeIntentProvider
import io.element.android.libraries.push.impl.notifications.shortcut.createShortcutId import io.element.android.libraries.push.impl.notifications.shortcut.createShortcutId
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
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.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader
import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationActionIds
@ -36,7 +37,6 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent 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.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.libraries.push.test.notifications.FakeImageLoader
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
@ -65,6 +65,7 @@ class DefaultNotificationCreatorTest {
fun `test createFallbackNotification`() { fun `test createFallbackNotification`() {
val sut = createNotificationCreator() val sut = createNotificationCreator()
val result = sut.createFallbackNotification( val result = sut.createFallbackNotification(
notificationAccountParams = aNotificationAccountParams(),
FallbackNotifiableEvent( FallbackNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -77,7 +78,6 @@ class DefaultNotificationCreatorTest {
timestamp = A_FAKE_TIMESTAMP, timestamp = A_FAKE_TIMESTAMP,
cause = null, cause = null,
), ),
color = A_COLOR_INT,
) )
result.commonAssertions( result.commonAssertions(
expectedCategory = null, expectedCategory = null,
@ -88,6 +88,7 @@ class DefaultNotificationCreatorTest {
fun `test createSimpleEventNotification`() { fun `test createSimpleEventNotification`() {
val sut = createNotificationCreator() val sut = createNotificationCreator()
val result = sut.createSimpleEventNotification( val result = sut.createSimpleEventNotification(
notificationAccountParams = aNotificationAccountParams(),
SimpleNotifiableEvent( SimpleNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -103,7 +104,6 @@ class DefaultNotificationCreatorTest {
isRedacted = false, isRedacted = false,
isUpdated = false, isUpdated = false,
), ),
color = A_COLOR_INT,
) )
result.commonAssertions( result.commonAssertions(
expectedCategory = null, expectedCategory = null,
@ -114,6 +114,7 @@ class DefaultNotificationCreatorTest {
fun `test createSimpleEventNotification noisy`() { fun `test createSimpleEventNotification noisy`() {
val sut = createNotificationCreator() val sut = createNotificationCreator()
val result = sut.createSimpleEventNotification( val result = sut.createSimpleEventNotification(
notificationAccountParams = aNotificationAccountParams(),
SimpleNotifiableEvent( SimpleNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -129,7 +130,6 @@ class DefaultNotificationCreatorTest {
isRedacted = false, isRedacted = false,
isUpdated = false, isUpdated = false,
), ),
color = A_COLOR_INT,
) )
result.commonAssertions( result.commonAssertions(
expectedCategory = null, expectedCategory = null,
@ -140,6 +140,7 @@ class DefaultNotificationCreatorTest {
fun `test createRoomInvitationNotification`() { fun `test createRoomInvitationNotification`() {
val sut = createNotificationCreator() val sut = createNotificationCreator()
val result = sut.createRoomInvitationNotification( val result = sut.createRoomInvitationNotification(
notificationAccountParams = aNotificationAccountParams(),
InviteNotifiableEvent( InviteNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -156,7 +157,6 @@ class DefaultNotificationCreatorTest {
isUpdated = false, isUpdated = false,
roomName = "roomName", roomName = "roomName",
), ),
color = A_COLOR_INT,
) )
result.commonAssertions( result.commonAssertions(
expectedCategory = null, expectedCategory = null,
@ -174,6 +174,7 @@ class DefaultNotificationCreatorTest {
fun `test createRoomInvitationNotification noisy`() { fun `test createRoomInvitationNotification noisy`() {
val sut = createNotificationCreator() val sut = createNotificationCreator()
val result = sut.createRoomInvitationNotification( val result = sut.createRoomInvitationNotification(
notificationAccountParams = aNotificationAccountParams(),
InviteNotifiableEvent( InviteNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -190,7 +191,6 @@ class DefaultNotificationCreatorTest {
isUpdated = false, isUpdated = false,
roomName = "roomName", roomName = "roomName",
), ),
color = A_COLOR_INT,
) )
result.commonAssertions( result.commonAssertions(
expectedCategory = null, expectedCategory = null,
@ -202,11 +202,10 @@ class DefaultNotificationCreatorTest {
val sut = createNotificationCreator() val sut = createNotificationCreator()
val matrixUser = aMatrixUser() val matrixUser = aMatrixUser()
val result = sut.createSummaryListNotification( val result = sut.createSummaryListNotification(
currentUser = matrixUser, notificationAccountParams = aNotificationAccountParams(user = matrixUser),
compatSummary = "compatSummary", compatSummary = "compatSummary",
noisy = false, noisy = false,
lastMessageTimestamp = 123_456L, lastMessageTimestamp = 123_456L,
color = A_COLOR_INT,
) )
result.commonAssertions( result.commonAssertions(
expectedGroup = matrixUser.userId.value, expectedGroup = matrixUser.userId.value,
@ -218,11 +217,10 @@ class DefaultNotificationCreatorTest {
val sut = createNotificationCreator() val sut = createNotificationCreator()
val matrixUser = aMatrixUser() val matrixUser = aMatrixUser()
val result = sut.createSummaryListNotification( val result = sut.createSummaryListNotification(
currentUser = matrixUser, notificationAccountParams = aNotificationAccountParams(user = matrixUser),
compatSummary = "compatSummary", compatSummary = "compatSummary",
noisy = true, noisy = true,
lastMessageTimestamp = 123_456L, lastMessageTimestamp = 123_456L,
color = A_COLOR_INT,
) )
result.commonAssertions( result.commonAssertions(
expectedGroup = matrixUser.userId.value, expectedGroup = matrixUser.userId.value,
@ -232,8 +230,8 @@ class DefaultNotificationCreatorTest {
@Test @Test
fun `test createMessagesListNotification`() = runTest { fun `test createMessagesListNotification`() = runTest {
val sut = createNotificationCreator() val sut = createNotificationCreator()
aMatrixUser()
val result = sut.createMessagesListNotification( val result = sut.createMessagesListNotification(
notificationAccountParams = aNotificationAccountParams(),
roomInfo = RoomEventGroupInfo( roomInfo = RoomEventGroupInfo(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -247,11 +245,9 @@ class DefaultNotificationCreatorTest {
largeIcon = null, largeIcon = null,
lastMessageTimestamp = 123_456L, lastMessageTimestamp = 123_456L,
tickerText = "tickerText", tickerText = "tickerText",
currentUser = aMatrixUser(),
existingNotification = null, existingNotification = null,
imageLoader = FakeImageLoader().getImageLoader(), imageLoader = FakeImageLoader(),
events = listOf(aNotifiableMessageEvent()), events = listOf(aNotifiableMessageEvent()),
color = A_COLOR_INT,
) )
result.commonAssertions() result.commonAssertions()
} }
@ -259,8 +255,8 @@ class DefaultNotificationCreatorTest {
@Test @Test
fun `test createMessagesListNotification should bing and thread`() = runTest { fun `test createMessagesListNotification should bing and thread`() = runTest {
val sut = createNotificationCreator() val sut = createNotificationCreator()
aMatrixUser()
val result = sut.createMessagesListNotification( val result = sut.createMessagesListNotification(
notificationAccountParams = aNotificationAccountParams(),
roomInfo = RoomEventGroupInfo( roomInfo = RoomEventGroupInfo(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -274,17 +270,15 @@ class DefaultNotificationCreatorTest {
largeIcon = null, largeIcon = null,
lastMessageTimestamp = 123_456L, lastMessageTimestamp = 123_456L,
tickerText = "tickerText", tickerText = "tickerText",
currentUser = aMatrixUser(),
existingNotification = null, existingNotification = null,
imageLoader = FakeImageLoader().getImageLoader(), imageLoader = FakeImageLoader(),
events = listOf(aNotifiableMessageEvent()), events = listOf(aNotifiableMessageEvent()),
color = A_COLOR_INT,
) )
result.commonAssertions() result.commonAssertions()
} }
private fun Notification.commonAssertions( private fun Notification.commonAssertions(
expectedGroup: String? = A_SESSION_ID.value, expectedGroup: String? = aMatrixUser().userId.value,
expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE, expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE,
) { ) {
assertThat(contentIntent).isNotNull() assertThat(contentIntent).isNotNull()

View file

@ -0,0 +1,23 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.notifications.factories
import androidx.annotation.ColorInt
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_COLOR_INT
import io.element.android.libraries.matrix.ui.components.aMatrixUser
fun aNotificationAccountParams(
user: MatrixUser = aMatrixUser(),
@ColorInt color: Int = A_COLOR_INT,
showSessionId: Boolean = false,
) = NotificationAccountParams(
user = user,
color = color,
showSessionId = showSessionId,
)

View file

@ -12,81 +12,84 @@ import android.graphics.Bitmap
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import coil3.ImageLoader import coil3.ImageLoader
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.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
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.fixtures.A_NOTIFICATION import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent 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.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder
import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaAnyRecorder import io.element.android.tests.testutils.lambda.lambdaAnyRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
class FakeNotificationCreator( class FakeNotificationCreator(
var createMessagesListNotificationResult: LambdaListAnyParamsRecorder<Notification> = lambdaAnyRecorder { A_NOTIFICATION }, var createMessagesListNotificationResult: LambdaListAnyParamsRecorder<Notification> = lambdaAnyRecorder { A_NOTIFICATION },
var createRoomInvitationNotificationResult: LambdaOneParamRecorder<InviteNotifiableEvent, Notification> = lambdaRecorder { _ -> A_NOTIFICATION }, var createRoomInvitationNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, InviteNotifiableEvent, Notification> =
var createSimpleNotificationResult: LambdaOneParamRecorder<SimpleNotifiableEvent, Notification> = lambdaRecorder { _ -> A_NOTIFICATION }, lambdaRecorder { _, _ -> A_NOTIFICATION },
var createFallbackNotificationResult: LambdaOneParamRecorder<FallbackNotifiableEvent, Notification> = lambdaRecorder { _ -> A_NOTIFICATION }, var createSimpleNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, SimpleNotifiableEvent, Notification> =
var createSummaryListNotificationResult: LambdaFourParamsRecorder<MatrixUser, String, Boolean, Long, Notification> = lambdaRecorder { _, _ -> A_NOTIFICATION },
lambdaRecorder { _, _, _, _ -> A_NOTIFICATION }, var createFallbackNotificationResult: LambdaTwoParamsRecorder<NotificationAccountParams, FallbackNotifiableEvent, Notification> =
var createDiagnosticNotificationResult: LambdaNoParamRecorder<Notification> = lambdaRecorder<Notification> { A_NOTIFICATION }, lambdaRecorder { _, _ -> A_NOTIFICATION },
var createSummaryListNotificationResult: LambdaFiveParamsRecorder<
NotificationAccountParams, String, Boolean, Long, NotificationAccountParams, Notification
> = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION },
var createDiagnosticNotificationResult: LambdaOneParamRecorder<Int, Notification> =
lambdaRecorder<Int, Notification> { _ -> A_NOTIFICATION },
) : NotificationCreator { ) : NotificationCreator {
override suspend fun createMessagesListNotification( override suspend fun createMessagesListNotification(
notificationAccountParams: NotificationAccountParams,
roomInfo: RoomEventGroupInfo, roomInfo: RoomEventGroupInfo,
threadId: ThreadId?, threadId: ThreadId?,
largeIcon: Bitmap?, largeIcon: Bitmap?,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
tickerText: String, tickerText: String,
currentUser: MatrixUser,
existingNotification: Notification?, existingNotification: Notification?,
imageLoader: ImageLoader, imageLoader: ImageLoader,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
@ColorInt color: Int,
): Notification { ): Notification {
return createMessagesListNotificationResult( return createMessagesListNotificationResult(
listOf(roomInfo, threadId, largeIcon, lastMessageTimestamp, tickerText, currentUser, existingNotification, imageLoader, events) listOf(notificationAccountParams, roomInfo, threadId, largeIcon, lastMessageTimestamp, tickerText, existingNotification, imageLoader, events)
) )
} }
override fun createRoomInvitationNotification( override fun createRoomInvitationNotification(
notificationAccountParams: NotificationAccountParams,
inviteNotifiableEvent: InviteNotifiableEvent, inviteNotifiableEvent: InviteNotifiableEvent,
@ColorInt color: Int,
): Notification { ): Notification {
return createRoomInvitationNotificationResult(inviteNotifiableEvent) return createRoomInvitationNotificationResult(notificationAccountParams, inviteNotifiableEvent)
} }
override fun createSimpleEventNotification( override fun createSimpleEventNotification(
notificationAccountParams: NotificationAccountParams,
simpleNotifiableEvent: SimpleNotifiableEvent, simpleNotifiableEvent: SimpleNotifiableEvent,
@ColorInt color: Int,
): Notification { ): Notification {
return createSimpleNotificationResult(simpleNotifiableEvent) return createSimpleNotificationResult(notificationAccountParams, simpleNotifiableEvent)
} }
override fun createFallbackNotification( override fun createFallbackNotification(
notificationAccountParams: NotificationAccountParams,
fallbackNotifiableEvent: FallbackNotifiableEvent, fallbackNotifiableEvent: FallbackNotifiableEvent,
@ColorInt color: Int,
): Notification { ): Notification {
return createFallbackNotificationResult(fallbackNotifiableEvent) return createFallbackNotificationResult(notificationAccountParams, fallbackNotifiableEvent)
} }
override fun createSummaryListNotification( override fun createSummaryListNotification(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
@ColorInt color: Int,
): Notification { ): Notification {
return createSummaryListNotificationResult(currentUser, compatSummary, noisy, lastMessageTimestamp) return createSummaryListNotificationResult(notificationAccountParams, compatSummary, noisy, lastMessageTimestamp, notificationAccountParams)
} }
override fun createDiagnosticNotification( override fun createDiagnosticNotification(
@ColorInt color: Int, @ColorInt color: Int,
): Notification { ): Notification {
return createDiagnosticNotificationResult() return createDiagnosticNotificationResult(color)
} }
} }

View file

@ -7,13 +7,12 @@
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.libraries.push.impl.notifications.fake
import androidx.annotation.ColorInt
import coil3.ImageLoader import coil3.ImageLoader
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.NotificationDataFactory import io.element.android.libraries.push.impl.notifications.NotificationDataFactory
import io.element.android.libraries.push.impl.notifications.OneShotNotification import io.element.android.libraries.push.impl.notifications.OneShotNotification
import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.SummaryNotification import io.element.android.libraries.push.impl.notifications.SummaryNotification
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent 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.InviteNotifiableEvent
@ -25,14 +24,15 @@ import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
class FakeNotificationDataFactory( class FakeNotificationDataFactory(
var messageEventToNotificationsResult: LambdaThreeParamsRecorder<List<NotifiableMessageEvent>, MatrixUser, ImageLoader, List<RoomNotification>> = var messageEventToNotificationsResult: LambdaThreeParamsRecorder<
lambdaRecorder { _, _, _ -> emptyList() }, List<NotifiableMessageEvent>, ImageLoader, NotificationAccountParams, List<RoomNotification>
> = lambdaRecorder { _, _, _ -> emptyList() },
var summaryToNotificationsResult: LambdaFiveParamsRecorder< var summaryToNotificationsResult: LambdaFiveParamsRecorder<
MatrixUser,
List<RoomNotification>, List<RoomNotification>,
List<OneShotNotification>, List<OneShotNotification>,
List<OneShotNotification>, List<OneShotNotification>,
List<OneShotNotification>, List<OneShotNotification>,
NotificationAccountParams,
SummaryNotification SummaryNotification
> = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) }, > = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) },
var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() }, var inviteToNotificationsResult: LambdaOneParamRecorder<List<InviteNotifiableEvent>, List<OneShotNotification>> = lambdaRecorder { _ -> emptyList() },
@ -42,18 +42,17 @@ class FakeNotificationDataFactory(
) : NotificationDataFactory { ) : NotificationDataFactory {
override suspend fun toNotifications( override suspend fun toNotifications(
messages: List<NotifiableMessageEvent>, messages: List<NotifiableMessageEvent>,
currentUser: MatrixUser,
imageLoader: ImageLoader, imageLoader: ImageLoader,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<RoomNotification> { ): List<RoomNotification> {
return messageEventToNotificationsResult(messages, currentUser, imageLoader) return messageEventToNotificationsResult(messages, imageLoader, notificationAccountParams)
} }
@JvmName("toNotificationInvites") @JvmName("toNotificationInvites")
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications( override fun toNotifications(
invites: List<InviteNotifiableEvent>, invites: List<InviteNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> { ): List<OneShotNotification> {
return inviteToNotificationsResult(invites) return inviteToNotificationsResult(invites)
} }
@ -62,7 +61,7 @@ class FakeNotificationDataFactory(
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications( override fun toNotifications(
simpleEvents: List<SimpleNotifiableEvent>, simpleEvents: List<SimpleNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> { ): List<OneShotNotification> {
return simpleEventToNotificationsResult(simpleEvents) return simpleEventToNotificationsResult(simpleEvents)
} }
@ -71,25 +70,24 @@ class FakeNotificationDataFactory(
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications( override fun toNotifications(
fallback: List<FallbackNotifiableEvent>, fallback: List<FallbackNotifiableEvent>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): List<OneShotNotification> { ): List<OneShotNotification> {
return fallbackEventToNotificationsResult(fallback) return fallbackEventToNotificationsResult(fallback)
} }
override fun createSummaryNotification( override fun createSummaryNotification(
currentUser: MatrixUser,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>, fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int, notificationAccountParams: NotificationAccountParams,
): SummaryNotification { ): SummaryNotification {
return summaryToNotificationsResult( return summaryToNotificationsResult(
currentUser,
roomNotifications, roomNotifications,
invitationNotifications, invitationNotifications,
simpleNotifications, simpleNotifications,
fallbackNotifications, fallbackNotifications,
notificationAccountParams,
) )
} }
} }

View file

@ -19,17 +19,17 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
class FakeNotificationDisplayer( class FakeNotificationDisplayer(
var showNotificationMessageResult: LambdaThreeParamsRecorder<String?, Int, Notification, Boolean> = lambdaRecorder { _, _, _ -> true }, var showNotificationResult: LambdaThreeParamsRecorder<String?, Int, Notification, Boolean> = lambdaRecorder { _, _, _ -> true },
var cancelNotificationMessageResult: LambdaTwoParamsRecorder<String?, Int, Unit> = lambdaRecorder { _, _ -> }, var cancelNotificationResult: LambdaTwoParamsRecorder<String?, Int, Unit> = lambdaRecorder { _, _ -> },
var displayDiagnosticNotificationResult: LambdaOneParamRecorder<Notification, Boolean> = lambdaRecorder { _ -> true }, var displayDiagnosticNotificationResult: LambdaOneParamRecorder<Notification, Boolean> = lambdaRecorder { _ -> true },
var dismissDiagnosticNotificationResult: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> }, var dismissDiagnosticNotificationResult: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
) : NotificationDisplayer { ) : NotificationDisplayer {
override fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean { override fun showNotification(tag: String?, id: Int, notification: Notification): Boolean {
return showNotificationMessageResult(tag, id, notification) return showNotificationResult(tag, id, notification)
} }
override fun cancelNotificationMessage(tag: String?, id: Int) { override fun cancelNotification(tag: String?, id: Int) {
return cancelNotificationMessageResult(tag, id) return cancelNotificationResult(tag, id)
} }
override fun displayDiagnosticNotification(notification: Notification): Boolean { override fun displayDiagnosticNotification(notification: Notification): Boolean {
@ -41,7 +41,7 @@ class FakeNotificationDisplayer(
} }
fun verifySummaryCancelled(times: Int = 1) { fun verifySummaryCancelled(times: Int = 1) {
cancelNotificationMessageResult.assertions().isCalledExactly(times).withSequence( cancelNotificationResult.assertions().isCalledExactly(times).withSequence(
listOf(value(null), value(NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID))) listOf(value(null), value(NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)))
) )
} }

View file

@ -8,12 +8,11 @@
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification import android.app.Notification
import androidx.annotation.ColorInt
import coil3.ImageLoader import coil3.ImageLoader
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.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.tests.testutils.lambda.LambdaSixParamsRecorder import io.element.android.tests.testutils.lambda.LambdaSixParamsRecorder
@ -22,18 +21,18 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
// We just can't make the param types fit // We just can't make the param types fit
@Suppress("MaxLineLength", "ktlint:standard:max-line-length", "ktlint:standard:parameter-wrapping") @Suppress("MaxLineLength", "ktlint:standard:max-line-length", "ktlint:standard:parameter-wrapping")
class FakeRoomGroupMessageCreator( class FakeRoomGroupMessageCreator(
var createRoomMessageResult: LambdaSixParamsRecorder<MatrixUser, List<NotifiableMessageEvent>, RoomId, ThreadId?, ImageLoader, Notification?, Notification> = var createRoomMessageResult: LambdaSixParamsRecorder<
lambdaRecorder { _, _, _, _, _, _ -> A_NOTIFICATION } NotificationAccountParams, List<NotifiableMessageEvent>, RoomId, ThreadId?, ImageLoader, Notification?, Notification
> = lambdaRecorder { _, _, _, _, _, _ -> A_NOTIFICATION }
) : RoomGroupMessageCreator { ) : RoomGroupMessageCreator {
override suspend fun createRoomMessage( override suspend fun createRoomMessage(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
roomId: RoomId, roomId: RoomId,
threadId: ThreadId?, threadId: ThreadId?,
imageLoader: ImageLoader, imageLoader: ImageLoader,
existingNotification: Notification?, existingNotification: Notification?,
@ColorInt color: Int,
): Notification { ): Notification {
return createRoomMessageResult(currentUser, events, roomId, threadId, imageLoader, existingNotification) return createRoomMessageResult(notificationAccountParams, events, roomId, threadId, imageLoader, existingNotification)
} }
} }

View file

@ -8,30 +8,28 @@
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification import android.app.Notification
import androidx.annotation.ColorInt
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.OneShotNotification import io.element.android.libraries.push.impl.notifications.OneShotNotification
import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams
import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION
import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
class FakeSummaryGroupMessageCreator( class FakeSummaryGroupMessageCreator(
var createSummaryNotificationResult: LambdaFiveParamsRecorder< var createSummaryNotificationResult: LambdaFiveParamsRecorder<
MatrixUser, List<RoomNotification>, List<OneShotNotification>, List<OneShotNotification>, List<OneShotNotification>, Notification> = NotificationAccountParams, List<RoomNotification>, List<OneShotNotification>, List<OneShotNotification>, List<OneShotNotification>, Notification> =
lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION } lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }
) : SummaryGroupMessageCreator { ) : SummaryGroupMessageCreator {
override fun createSummaryNotification( override fun createSummaryNotification(
currentUser: MatrixUser, notificationAccountParams: NotificationAccountParams,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
fallbackNotifications: List<OneShotNotification>, fallbackNotifications: List<OneShotNotification>,
@ColorInt color: Int,
): Notification { ): Notification {
return createSummaryNotificationResult( return createSummaryNotificationResult(
currentUser, notificationAccountParams,
roomNotifications, roomNotifications,
invitationNotifications, invitationNotifications,
simpleNotifications, simpleNotifications,

View file

@ -122,7 +122,7 @@ class DefaultOnRedactedEventReceivedTest {
} }
) )
}, },
displayer = FakeNotificationDisplayer(showNotificationMessageResult = showNotificationLambda), displayer = FakeNotificationDisplayer(showNotificationResult = showNotificationLambda),
) )
sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))) sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)))

View file

@ -1,45 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.test.notifications
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import coil3.ImageLoader
import coil3.test.FakeImageLoaderEngine
import coil3.test.intercept
import org.robolectric.RuntimeEnvironment
class FakeImageLoader {
private val coilRequests = mutableListOf<Any>()
private var cache: ImageLoader? = null
fun getImageLoader(): ImageLoader {
return cache ?: ImageLoader.Builder(RuntimeEnvironment.getApplication())
.components {
val engine = FakeImageLoaderEngine.Builder()
.intercept(
predicate = {
coilRequests.add(it)
true
},
drawable = ColorDrawable(Color.BLUE)
)
.build()
add(engine)
}
.build()
.also {
cache = it
}
}
fun getCoilRequests(): List<Any> {
return coilRequests.toList()
}
}