diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 991f8dd117..eb6e9998ac 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -20,15 +20,23 @@ 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.UserId -//TODO add content data class NotificationData( val senderId: UserId, val eventId: EventId, val roomId: RoomId, - val senderAvatarUrl: String? = null, - val senderDisplayName: String? = null, - val roomAvatarUrl: String? = null, + val senderAvatarUrl: String?, + val senderDisplayName: String?, + val roomAvatarUrl: String?, + val roomDisplayName: String?, val isDirect: Boolean, val isEncrypted: Boolean, val isNoisy: Boolean, + val event: NotificationEvent, +) + +data class NotificationEvent( + val timestamp: Long, + val content: String, + // For images for instance + val contentUrl: String? ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt index 4b121db9bf..e6125cf69b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -23,9 +23,9 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationData import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.use -import javax.inject.Inject -class NotificationMapper @Inject constructor() { +class NotificationMapper { + private val timelineEventMapper = TimelineEventMapper() fun map(notificationItem: NotificationItem): NotificationData { return notificationItem.use { @@ -36,9 +36,11 @@ class NotificationMapper @Inject constructor() { senderAvatarUrl = it.senderAvatarUrl, senderDisplayName = it.senderDisplayName, roomAvatarUrl = it.roomAvatarUrl, + roomDisplayName = it.roomDisplayName, isDirect = it.isDirect, isEncrypted = it.isEncrypted.orFalse(), - isNoisy = it.isNoisy + isNoisy = it.isNoisy, + event = it.event.use { event -> timelineEventMapper.map(event) } ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt index bd94de21fc..8b630cd64a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt @@ -16,18 +16,13 @@ package io.element.android.libraries.matrix.impl.notification -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.MatrixClient 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.SessionId -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationService -import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.use -import java.io.File class RustNotificationService( private val client: Client, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt new file mode 100644 index 0000000000..adb9dcce72 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.notification + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.NotificationEvent +import org.matrix.rustcomponents.sdk.MessageLikeEventContent +import org.matrix.rustcomponents.sdk.MessageType +import org.matrix.rustcomponents.sdk.StateEventContent +import org.matrix.rustcomponents.sdk.TimelineEvent +import org.matrix.rustcomponents.sdk.TimelineEventType +import org.matrix.rustcomponents.sdk.use +import javax.inject.Inject + +class TimelineEventMapper @Inject constructor() { + + fun map(timelineEvent: TimelineEvent): NotificationEvent { + return timelineEvent.use { + NotificationEvent( + timestamp = it.timestamp().toLong(), + content = it.eventType().toContent(), + contentUrl = null // TODO it.eventType().toContentUrl(), + ) + } + } +} + +private fun TimelineEventType.toContent(): String { + return when (this) { + is TimelineEventType.MessageLike -> content.toContent() + is TimelineEventType.State -> content.toContent() + } +} + +private fun StateEventContent.toContent(): String { + return when (this) { + StateEventContent.PolicyRuleRoom -> "PolicyRuleRoom" + StateEventContent.PolicyRuleServer -> "PolicyRuleServer" + StateEventContent.PolicyRuleUser -> "PolicyRuleUser" + StateEventContent.RoomAliases -> "RoomAliases" + StateEventContent.RoomAvatar -> "RoomAvatar" + StateEventContent.RoomCanonicalAlias -> "RoomCanonicalAlias" + StateEventContent.RoomCreate -> "RoomCreate" + StateEventContent.RoomEncryption -> "RoomEncryption" + StateEventContent.RoomGuestAccess -> "RoomGuestAccess" + StateEventContent.RoomHistoryVisibility -> "RoomHistoryVisibility" + StateEventContent.RoomJoinRules -> "RoomJoinRules" + is StateEventContent.RoomMemberContent -> "$userId is now $membershipState" + StateEventContent.RoomName -> "RoomName" + StateEventContent.RoomPinnedEvents -> "RoomPinnedEvents" + StateEventContent.RoomPowerLevels -> "RoomPowerLevels" + StateEventContent.RoomServerAcl -> "RoomServerAcl" + StateEventContent.RoomThirdPartyInvite -> "RoomThirdPartyInvite" + StateEventContent.RoomTombstone -> "RoomTombstone" + StateEventContent.RoomTopic -> "RoomTopic" + StateEventContent.SpaceChild -> "SpaceChild" + StateEventContent.SpaceParent -> "SpaceParent" + } +} + +private fun MessageLikeEventContent.toContent(): String { + return use { + when (it) { + MessageLikeEventContent.CallAnswer -> "CallAnswer" + MessageLikeEventContent.CallCandidates -> "CallCandidates" + MessageLikeEventContent.CallHangup -> "CallHangup" + MessageLikeEventContent.CallInvite -> "CallInvite" + MessageLikeEventContent.KeyVerificationAccept -> "KeyVerificationAccept" + MessageLikeEventContent.KeyVerificationCancel -> "KeyVerificationCancel" + MessageLikeEventContent.KeyVerificationDone -> "KeyVerificationDone" + MessageLikeEventContent.KeyVerificationKey -> "KeyVerificationKey" + MessageLikeEventContent.KeyVerificationMac -> "KeyVerificationMac" + MessageLikeEventContent.KeyVerificationReady -> "KeyVerificationReady" + MessageLikeEventContent.KeyVerificationStart -> "KeyVerificationStart" + is MessageLikeEventContent.ReactionContent -> "Reacted to ${it.relatedEventId.take(8)}…" + MessageLikeEventContent.RoomEncrypted -> "RoomEncrypted" + is MessageLikeEventContent.RoomMessage -> it.messageType.toContent() + MessageLikeEventContent.RoomRedaction -> "RoomRedaction" + MessageLikeEventContent.Sticker -> "Sticker" + } + } +} + +private fun MessageType.toContent(): String { + return when (this) { + is MessageType.Audio -> content.use { it.body } + is MessageType.Emote -> content.body + is MessageType.File -> content.use { it.body } + is MessageType.Image -> content.use { it.body } + is MessageType.Notice -> content.body + is MessageType.Text -> content.body + is MessageType.Video -> content.use { it.body } + } +} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 2951ca0e25..725961a248 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.androidx.security.crypto) implementation(libs.network.retrofit) implementation(libs.serialization.json) + implementation(libs.coil) implementation(projects.libraries.architecture) implementation(projects.libraries.core) @@ -42,6 +43,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.network) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) api(projects.libraries.pushproviders.api) api(projects.libraries.pushstore.api) api(projects.libraries.push.api) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index de168090f4..fb3fcfc61f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -24,6 +24,7 @@ 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.UserId import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.notification.NotificationEvent import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -44,9 +45,9 @@ class NotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, // private val noticeEventFormatter: NoticeEventFormatter, // private val displayableEventFormatter: DisplayableEventFormatter, - private val clock: SystemClock, private val matrixAuthenticationService: MatrixAuthenticationService, private val buildMeta: BuildMeta, + private val clock: SystemClock, ) { suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { @@ -80,14 +81,14 @@ class NotifiableEventResolver @Inject constructor( editedEventId = null, canBeReplaced = true, noisy = isNoisy, - timestamp = clock.epochMillis(), + timestamp = event.timestamp, senderName = senderDisplayName, senderId = senderId.value, - body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…", - imageUriString = null, + body = event.content, + imageUriString = event.contentUrl, threadId = null, - roomName = null, - roomIsDirect = false, + roomName = roomDisplayName, + roomIsDirect = isDirect, roomAvatarPath = roomAvatarUrl, senderAvatarPath = senderAvatarUrl, soundName = null, @@ -97,18 +98,27 @@ class NotifiableEventResolver @Inject constructor( isUpdated = false ) } -} -/** - * TODO This is a temporary method for EAx. - */ -private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData { - return this ?: NotificationData( - eventId = eventId, - senderId = UserId("@user:domain"), - roomId = roomId, - isNoisy = false, - isEncrypted = false, - isDirect = false - ) + /** + * TODO This is a temporary method for EAx. + */ + private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData { + return this ?: NotificationData( + eventId = eventId, + senderId = UserId("@user:domain"), + roomId = roomId, + senderAvatarUrl = null, + senderDisplayName = null, + roomAvatarUrl = null, + roomDisplayName = null, + isNoisy = false, + isEncrypted = false, + isDirect = false, + event = NotificationEvent( + timestamp = clock.epochMillis(), + content = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…", + contentUrl = null + ) + ) + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt index 7bd76f9f42..c2cdfc5677 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt @@ -19,9 +19,14 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import android.graphics.Bitmap import android.os.Build -import androidx.annotation.WorkerThread import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toBitmap +import coil.imageLoader +import coil.request.ImageRequest +import coil.transform.CircleCropTransformation import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.ui.media.MediaRequestData import timber.log.Timber import javax.inject.Inject @@ -31,30 +36,24 @@ class NotificationBitmapLoader @Inject constructor( /** * Get icon of a room. + * @param path mxc url */ - @WorkerThread - fun getRoomBitmap(path: String?): Bitmap? { + suspend fun getRoomBitmap(path: String?): Bitmap? { if (path == null) { return null } return loadRoomBitmap(path) } - @WorkerThread - private fun loadRoomBitmap(path: String): Bitmap? { + private suspend fun loadRoomBitmap(path: String): Bitmap? { return try { - null - /* TODO Notification - Glide.with(context) - .asBitmap() - .load(path) - .format(DecodeFormat.PREFER_ARGB_8888) - .signature(ObjectKey("room-icon-notification")) - .submit() - .get() - */ - } catch (e: Exception) { - Timber.e(e, "decodeFile failed") + val imageRequest = ImageRequest.Builder(context) + .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) + .build() + val result = context.imageLoader.execute(imageRequest) + result.drawable?.toBitmap() + } catch (e: Throwable) { + Timber.e(e, "Unable to load room bitmap") null } } @@ -62,9 +61,9 @@ class NotificationBitmapLoader @Inject constructor( /** * Get icon of a user. * Before Android P, this does nothing because the icon won't be used + * @param path mxc url */ - @WorkerThread - fun getUserIcon(path: String?): IconCompat? { + suspend fun getUserIcon(path: String?): IconCompat? { if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { return null } @@ -72,23 +71,17 @@ class NotificationBitmapLoader @Inject constructor( return loadUserIcon(path) } - @WorkerThread - private fun loadUserIcon(path: String): IconCompat? { + private suspend fun loadUserIcon(path: String): IconCompat? { return try { - null - /* TODO Notification - val bitmap = Glide.with(context) - .asBitmap() - .load(path) - .transform(CircleCrop()) - .format(DecodeFormat.PREFER_ARGB_8888) - .signature(ObjectKey("user-icon-notification")) - .submit() - .get() - IconCompat.createWithBitmap(bitmap) - */ - } catch (e: Exception) { - Timber.e(e, "decodeFile failed") + val imageRequest = ImageRequest.Builder(context) + .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) + .transformations(CircleCropTransformation()) + .build() + val result = context.imageLoader.execute(imageRequest) + val bitmap = result.drawable?.toBitmap() + return bitmap?.let { IconCompat.createWithBitmap(it) } + } catch (e: Throwable) { + Timber.e(e, "Unable to load user bitmap") null } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt index cf0307fbd9..87d37e7e33 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt @@ -16,28 +16,28 @@ package io.element.android.libraries.push.impl.notifications -import android.content.Context -import android.os.Handler -import android.os.HandlerThread -import androidx.annotation.WorkerThread import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.core.cache.CircularCache +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService 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.ThreadId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.store.PushDataStore -import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -48,7 +48,6 @@ import javax.inject.Inject */ @SingleIn(AppScope::class) class NotificationDrawerManager @Inject constructor( - @ApplicationContext context: Context, private val pushDataStore: PushDataStore, private val notifiableEventProcessor: NotifiableEventProcessor, private val notificationRenderer: NotificationRenderer, @@ -56,17 +55,14 @@ class NotificationDrawerManager @Inject constructor( private val filteredEventDetector: FilteredEventDetector, private val appNavigationStateService: AppNavigationStateService, private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatchers, private val buildMeta: BuildMeta, + private val matrixAuthenticationService: MatrixAuthenticationService, ) { - - private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY) - private var backgroundHandler: Handler - /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. */ private val notificationState by lazy { createInitialNotificationState() } - private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentAppNavigationState: AppNavigationState? = null private val firstThrottler = FirstThrottler(200) @@ -74,8 +70,6 @@ class NotificationDrawerManager @Inject constructor( private var useCompleteNotificationFormat = true init { - handlerThread.start() - backgroundHandler = Handler(handlerThread.looper) // Observe application state coroutineScope.launch { appNavigationStateService.appNavigationStateFlow @@ -193,30 +187,25 @@ class NotificationDrawerManager @Inject constructor( notificationState.updateQueuedEvents(this) { queuedEvents, _ -> action(queuedEvents) } - refreshNotificationDrawer() + coroutineScope.refreshNotificationDrawer() } - private fun refreshNotificationDrawer() { + private fun CoroutineScope.refreshNotificationDrawer() = launch { // Implement last throttler val canHandle = firstThrottler.canHandle() Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") - backgroundHandler.removeCallbacksAndMessages(null) - - backgroundHandler.postDelayed( - { - try { - refreshNotificationDrawerBg() - } catch (throwable: Throwable) { - // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer - Timber.w(throwable, "refreshNotificationDrawerBg failure") - } - }, - canHandle.waitMillis() - ) + withContext(dispatchers.io) { + delay(canHandle.waitMillis()) + try { + refreshNotificationDrawerBg() + } catch (throwable: Throwable) { + // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer + Timber.w(throwable, "refreshNotificationDrawerBg failure") + } + } } - @WorkerThread - private fun refreshNotificationDrawerBg() { + private suspend fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also { @@ -239,24 +228,34 @@ class NotificationDrawerManager @Inject constructor( } } - private fun renderEvents(eventsToRender: List>) { + private suspend fun renderEvents(eventsToRender: List>) { // Group by sessionId val eventsForSessions = eventsToRender.groupBy { it.event.sessionId } eventsForSessions.forEach { (sessionId, notifiableEvents) -> - // TODO EAx val user = session.getUserOrDefault(session.myUserId) - // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash - val myUserDisplayName = "Todo display name" // user.toMatrixItem().getBestName() - // TODO EAx avatar URL - val myUserAvatarUrl = null // session.contentUrlResolver().resolveThumbnail( - // contentUrl = user.avatarUrl, - // width = avatarSize, - // height = avatarSize, - // method = ContentUrlResolver.ThumbnailMethod.SCALE - //) - notificationRenderer.render(sessionId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, notifiableEvents) + val currentUser = tryOrNull( + onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") }, + operation = { + val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull() + + // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash + val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value + val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull() + MatrixUser( + userId = sessionId, + displayName = myUserDisplayName, + avatarUrl = userAvatarUrl + ) + } + ) ?: MatrixUser( + userId = sessionId, + displayName = sessionId.value, + avatarUrl = null + ) + + notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index 4bb49e168f..79173611dc 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification 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.user.MatrixUser import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -34,10 +34,8 @@ class NotificationFactory @Inject constructor( private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { - fun Map.toNotifications( - sessionId: SessionId, - myUserDisplayName: String, - myUserAvatarUrl: String? + suspend fun Map.toNotifications( + currentUser: MatrixUser, ): List { return map { (roomId, events) -> when { @@ -45,11 +43,9 @@ class NotificationFactory @Inject constructor( else -> { val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted } roomGroupMessageCreator.createRoomMessage( - sessionId = sessionId, + currentUser = currentUser, events = messageEvents, roomId = roomId, - userDisplayName = myUserDisplayName, - userAvatarUrl = myUserAvatarUrl ) } } @@ -99,7 +95,7 @@ class NotificationFactory @Inject constructor( } fun createSummaryNotification( - sessionId: SessionId, + currentUser: MatrixUser, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, @@ -112,7 +108,7 @@ class NotificationFactory @Inject constructor( roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed else -> SummaryNotification.Update( summaryGroupMessageCreator.createSummaryNotification( - sessionId = sessionId, + currentUser = currentUser, roomNotifications = roomMeta, invitationNotifications = invitationMeta, simpleNotifications = simpleMeta, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index 277dc3b822..428420211b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -16,9 +16,8 @@ package io.element.android.libraries.push.impl.notifications -import androidx.annotation.WorkerThread 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.user.MatrixUser import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -32,21 +31,18 @@ class NotificationRenderer @Inject constructor( private val notificationFactory: NotificationFactory, ) { - @WorkerThread - fun render( - sessionId: SessionId, - myUserDisplayName: String, - myUserAvatarUrl: String?, + suspend fun render( + currentUser: MatrixUser, useCompleteNotificationFormat: Boolean, eventsToProcess: List> ) { val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { - val roomNotifications = roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) + val roomNotifications = roomEvents.toNotifications(currentUser) val invitationNotifications = invitationEvents.toNotifications() val simpleNotifications = simpleEvents.toNotifications() val summaryNotification = createSummaryNotification( - sessionId = sessionId, + currentUser = currentUser, roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, @@ -56,21 +52,27 @@ class NotificationRenderer @Inject constructor( // Remove summary first to avoid briefly displaying it after dismissing the last notification if (summaryNotification == SummaryNotification.Removed) { Timber.d("Removing summary notification") - notificationDisplayer.cancelNotificationMessage(null, notificationIdProvider.getSummaryNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = null, + id = notificationIdProvider.getSummaryNotificationId(currentUser.userId) + ) } roomNotifications.forEach { wrapper -> when (wrapper) { is RoomNotification.Removed -> { Timber.d("Removing room messages notification ${wrapper.roomId}") - notificationDisplayer.cancelNotificationMessage(wrapper.roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = wrapper.roomId.value, + id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId) + ) } is RoomNotification.Message -> if (useCompleteNotificationFormat) { Timber.d("Updating room messages notification ${wrapper.meta.roomId}") notificationDisplayer.showNotificationMessage( - wrapper.meta.roomId.value, - notificationIdProvider.getRoomMessagesNotificationId(sessionId), - wrapper.notification + tag = wrapper.meta.roomId.value, + id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId), + notification = wrapper.notification ) } } @@ -80,14 +82,17 @@ class NotificationRenderer @Inject constructor( when (wrapper) { is OneShotNotification.Removed -> { Timber.d("Removing invitation notification ${wrapper.key}") - notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomInvitationNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = wrapper.key, + id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId) + ) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating invitation notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage( - wrapper.meta.key, - notificationIdProvider.getRoomInvitationNotificationId(sessionId), - wrapper.notification + tag = wrapper.meta.key, + id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), + notification = wrapper.notification ) } } @@ -97,14 +102,17 @@ class NotificationRenderer @Inject constructor( when (wrapper) { is OneShotNotification.Removed -> { Timber.d("Removing simple notification ${wrapper.key}") - notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomEventNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = wrapper.key, + id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId) + ) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating simple notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage( - wrapper.meta.key, - notificationIdProvider.getRoomEventNotificationId(sessionId), - wrapper.notification + tag = wrapper.meta.key, + id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId), + notification = wrapper.notification ) } } @@ -114,9 +122,9 @@ class NotificationRenderer @Inject constructor( if (summaryNotification is SummaryNotification.Update) { Timber.d("Updating summary notification") notificationDisplayer.showNotificationMessage( - null, - notificationIdProvider.getSummaryNotificationId(sessionId), - summaryNotification.notification + tag = null, + id = notificationIdProvider.getSummaryNotificationId(currentUser.userId), + notification = summaryNotification.notification ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 00222728bf..5656b81dd9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -20,8 +20,9 @@ import android.graphics.Bitmap import androidx.core.app.NotificationCompat import androidx.core.app.Person 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.user.MatrixUser import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider @@ -36,24 +37,22 @@ class RoomGroupMessageCreator @Inject constructor( private val notificationFactory: NotificationFactory ) { - fun createRoomMessage( - sessionId: SessionId, + suspend fun createRoomMessage( + currentUser: MatrixUser, events: List, roomId: RoomId, - userDisplayName: String, - userAvatarUrl: String? ): RoomNotification.Message { val lastKnownRoomEvent = events.last() val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)" val roomIsGroup = !lastKnownRoomEvent.roomIsDirect val style = NotificationCompat.MessagingStyle( Person.Builder() - .setName(userDisplayName) - .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) + .setName(currentUser.displayName?.annotateForDebug(50)) + .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl)) .setKey(lastKnownRoomEvent.sessionId.value) .build() ).also { - it.conversationTitle = roomName.takeIf { roomIsGroup } + it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51) it.isGroupConversation = roomIsGroup it.addMessagesFromEvents(events) } @@ -80,7 +79,7 @@ class RoomGroupMessageCreator @Inject constructor( notificationFactory.createMessagesListNotification( style, RoomEventGroupInfo( - sessionId = sessionId, + sessionId = currentUser.userId, roomId = roomId, roomDisplayName = roomName, isDirect = !roomIsGroup, @@ -99,13 +98,13 @@ class RoomGroupMessageCreator @Inject constructor( ) } - private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { + private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { events.forEach { event -> val senderPerson = if (event.outGoingMessage) { null } else { Person.Builder() - .setName(event.senderName) + .setName(event.senderName?.annotateForDebug(70)) .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath)) .setKey(event.senderId) .build() @@ -117,7 +116,11 @@ class RoomGroupMessageCreator @Inject constructor( senderPerson ) else -> { - val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message -> + val message = NotificationCompat.MessagingStyle.Message( + event.body?.annotateForDebug(71), + event.timestamp, + senderPerson + ).also { message -> event.imageUri?.let { message.setData("image/", it) } @@ -168,7 +171,7 @@ class RoomGroupMessageCreator @Inject constructor( } } - private fun getRoomBitmap(events: List): Bitmap? { + private suspend fun getRoomBitmap(events: List): Bitmap? { // Use the last event (most recent?) return events.lastOrNull() ?.roomAvatarPath diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index a400c2b7a3..5a7f3d36e8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -18,8 +18,9 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import androidx.core.app.NotificationCompat -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject @@ -40,20 +41,20 @@ import javax.inject.Inject */ class SummaryGroupMessageCreator @Inject constructor( private val stringProvider: StringProvider, - private val notificationFactory: NotificationFactory + private val notificationFactory: NotificationFactory, ) { fun createSummaryNotification( - sessionId: SessionId, + currentUser: MatrixUser, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, useCompleteNotificationFormat: Boolean ): Notification { val summaryInboxStyle = NotificationCompat.InboxStyle().also { style -> - roomNotifications.forEach { style.addLine(it.summaryLine) } - invitationNotifications.forEach { style.addLine(it.summaryLine) } - simpleNotifications.forEach { style.addLine(it.summaryLine) } + roomNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(40)) } + invitationNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(41)) } + simpleNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(42)) } } val summaryIsNoisy = roomNotifications.any { it.shouldBing } || @@ -69,12 +70,13 @@ class SummaryGroupMessageCreator @Inject constructor( // 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) - summaryInboxStyle.setBigContentTitle(sumTitle) - // TODO get latest event? - .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + summaryInboxStyle.setBigContentTitle(sumTitle.annotateForDebug(43)) + //.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents).annotateForDebug(44)) + // Use account name now, for multi-session + .setSummaryText(currentUser.userId.value.annotateForDebug(44)) return if (useCompleteNotificationFormat) { notificationFactory.createSummaryListNotification( - sessionId, + currentUser, summaryInboxStyle, sumTitle, noisy = summaryIsNoisy, @@ -82,7 +84,7 @@ class SummaryGroupMessageCreator @Inject constructor( ) } else { processSimpleGroupSummary( - sessionId, + currentUser, summaryIsNoisy, messageCount, simpleNotifications.size, @@ -94,7 +96,7 @@ class SummaryGroupMessageCreator @Inject constructor( } private fun processSimpleGroupSummary( - sessionId: SessionId, + currentUser: MatrixUser, summaryIsNoisy: Boolean, messageEventsCount: Int, simpleEventsCount: Int, @@ -167,7 +169,7 @@ class SummaryGroupMessageCreator @Inject constructor( } } return notificationFactory.createSummaryListNotification( - sessionId = sessionId, + currentUser = currentUser, style = null, compatSummary = privacyTitle, noisy = summaryIsNoisy, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt new file mode 100644 index 0000000000..37f33e1188 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications.debug + +fun CharSequence.annotateForDebug(@Suppress("UNUSED_PARAMETER") prefix: Int): CharSequence { + return this // "$prefix-$this" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt index 5795ea5f5f..9da47a6569 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt @@ -26,11 +26,12 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.ApplicationContext -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.user.MatrixUser 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.channels.NotificationChannels +import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory @@ -84,16 +85,16 @@ class NotificationFactory @Inject constructor( // ID of the corresponding shortcut, for conversation features under API 30+ .setShortcutId(roomInfo.roomId.value) // Title for API < 16 devices. - .setContentTitle(roomInfo.roomDisplayName) + .setContentTitle(roomInfo.roomDisplayName.annotateForDebug(1)) // Content for API < 16 devices. - .setContentText(stringProvider.getString(R.string.notification_new_messages)) + .setContentText(stringProvider.getString(R.string.notification_new_messages).annotateForDebug(2)) // Number of new notifications for API <24 (M and below) devices. .setSubText( stringProvider.getQuantityString( R.plurals.notification_new_messages_for_room, messageStyle.messages.size, messageStyle.messages.size - ) + ).annotateForDebug(3) ) // 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 @@ -135,7 +136,7 @@ class NotificationFactory @Inject constructor( } setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) } - .setTicker(tickerText) + .setTicker(tickerText.annotateForDebug(4)) .build() } @@ -147,8 +148,8 @@ class NotificationFactory @Inject constructor( val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) - .setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) - .setContentText(inviteNotifiableEvent.description) + .setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5)) + .setContentText(inviteNotifiableEvent.description.annotateForDebug(6)) .setGroup(inviteNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) @@ -196,8 +197,8 @@ class NotificationFactory @Inject constructor( val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) - .setContentTitle(buildMeta.applicationName) - .setContentText(simpleNotifiableEvent.description) + .setContentTitle(buildMeta.applicationName.annotateForDebug(7)) + .setContentText(simpleNotifiableEvent.description.annotateForDebug(8)) .setGroup(simpleNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) @@ -226,7 +227,7 @@ class NotificationFactory @Inject constructor( * Create the summary notification. */ fun createSummaryListNotification( - sessionId: SessionId, + currentUser: MatrixUser, style: NotificationCompat.InboxStyle?, compatSummary: String, noisy: Boolean, @@ -240,12 +241,12 @@ class NotificationFactory @Inject constructor( // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setStyle(style) - .setContentTitle(sessionId.value) + .setContentTitle(currentUser.userId.value.annotateForDebug(9)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(smallIcon) // set content text to support devices running API level < 24 - .setContentText(compatSummary) - .setGroup(sessionId.value) + .setContentText(compatSummary.annotateForDebug(10)) + .setGroup(currentUser.userId.value) // set this notification as the summary for the group .setGroupSummary(true) .setColor(accentColor) @@ -264,8 +265,8 @@ class NotificationFactory @Inject constructor( priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId)) - .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId)) + .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId)) + .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId)) .build() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt index d7af528ce3..1216e0fe12 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt @@ -57,6 +57,9 @@ data class NotifiableMessageEvent( val description: String = body ?: "" val title: String = senderName ?: "" + // TODO EAx The image has to be downloaded and expose using the file provider. + // Example of value from Element Android: + // content://im.vector.app.debug.mx-sdk.fileprovider/downloads/downloads/816abf76d806c768760568952b1862c8/F/72c33edd23dee3b95f4d5a18aa25fa54/image.png val imageUri: Uri? get() = imageUriString?.let { Uri.parse(it) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt index 9fbd723071..18d8870ac3 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId +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.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -27,6 +28,7 @@ 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.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent +import kotlinx.coroutines.test.runTest import org.junit.Test private val MY_AVATAR_URL: String? = null @@ -124,11 +126,13 @@ class NotificationFactoryTest { fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) { val events = listOf(A_MESSAGE_EVENT) val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( - A_SESSION_ID, events, A_ROOM_ID, A_SESSION_ID.value, MY_AVATAR_URL + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), events, A_ROOM_ID ) val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT))) - val result = roomWithMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = roomWithMessage.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo(listOf(expectedNotification)) } @@ -138,7 +142,9 @@ class NotificationFactoryTest { val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT)) val emptyRoom = mapOf(A_ROOM_ID to events) - val result = emptyRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = emptyRoom.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo( listOf( @@ -153,7 +159,9 @@ class NotificationFactoryTest { fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)))) - val result = redactedRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = redactedRoom.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo( listOf( @@ -176,19 +184,21 @@ class NotificationFactoryTest { ) val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted"))) val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( - A_SESSION_ID, + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), withRedactedRemoved, A_ROOM_ID, - A_SESSION_ID.value, - MY_AVATAR_URL ) - val result = roomWithRedactedMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = roomWithRedactedMessage.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo(listOf(expectedNotification)) } } -fun testWith(receiver: T, block: T.() -> Unit) { - receiver.block() +fun testWith(receiver: T, block: suspend T.() -> Unit) { + runTest { + receiver.block() + } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt index 79c6dfdb02..c109edb40a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification +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.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -24,6 +25,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test private const val MY_USER_DISPLAY_NAME = "display-name" @@ -53,7 +55,7 @@ class NotificationRendererTest { ) @Test - fun `given no notifications when rendering then cancels summary notification`() { + fun `given no notifications when rendering then cancels summary notification`() = runTest { givenNoNotifications() renderEventsAsNotifications() @@ -63,7 +65,7 @@ class NotificationRendererTest { } @Test - fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() { + fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() = runTest { givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) renderEventsAsNotifications() @@ -75,7 +77,7 @@ class NotificationRendererTest { } @Test - fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() { + fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() = runTest { givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID))) renderEventsAsNotifications() @@ -87,7 +89,7 @@ class NotificationRendererTest { } @Test - fun `given a room message group notification is added when rendering then show the message notification and update summary`() { + fun `given a room message group notification is added when rendering then show the message notification and update summary`() = runTest { givenNotifications( roomNotifications = listOf( RoomNotification.Message( @@ -106,7 +108,7 @@ class NotificationRendererTest { } @Test - fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() { + fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() = runTest { givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) renderEventsAsNotifications() @@ -118,7 +120,7 @@ class NotificationRendererTest { } @Test - fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() { + fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() = runTest { givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value))) renderEventsAsNotifications() @@ -130,7 +132,7 @@ class NotificationRendererTest { } @Test - fun `given a simple notification is added when rendering then show the simple notification and update summary`() { + fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest { givenNotifications( simpleNotifications = listOf( OneShotNotification.Append( @@ -149,7 +151,7 @@ class NotificationRendererTest { } @Test - fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() { + fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() = runTest { givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) renderEventsAsNotifications() @@ -161,7 +163,7 @@ class NotificationRendererTest { } @Test - fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() { + fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() = runTest { givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value))) renderEventsAsNotifications() @@ -173,7 +175,7 @@ class NotificationRendererTest { } @Test - fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() { + fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest { givenNotifications( simpleNotifications = listOf( OneShotNotification.Append( @@ -191,11 +193,9 @@ class NotificationRendererTest { } } - private fun renderEventsAsNotifications() { + private suspend fun renderEventsAsNotifications() { notificationRenderer.render( - sessionId = A_SESSION_ID, - myUserDisplayName = MY_USER_DISPLAY_NAME, - myUserAvatarUrl = MY_USER_AVATAR_URL, + MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, eventsToProcess = AN_EVENT_LIST ) @@ -214,9 +214,7 @@ class NotificationRendererTest { ) { notificationFactory.givenNotificationsFor( groupedEvents = A_PROCESSED_EVENTS, - sessionId = A_SESSION_ID, - myUserDisplayName = MY_USER_DISPLAY_NAME, - myUserAvatarUrl = MY_USER_AVATAR_URL, + matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), useCompleteNotificationFormat = useCompleteNotificationFormat, roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt index 7d7812e6cb..09957e2cf2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt @@ -16,12 +16,13 @@ package io.element.android.libraries.push.impl.notifications.fake -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.GroupedNotificationEvents import io.element.android.libraries.push.impl.notifications.NotificationFactory 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.SummaryNotification +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -30,9 +31,7 @@ class FakeNotificationFactory { fun givenNotificationsFor( groupedEvents: GroupedNotificationEvents, - sessionId: SessionId, - myUserDisplayName: String, - myUserAvatarUrl: String?, + matrixUser: MatrixUser, useCompleteNotificationFormat: Boolean, roomNotifications: List, invitationNotifications: List, @@ -40,13 +39,13 @@ class FakeNotificationFactory { summaryNotification: SummaryNotification ) { with(instance) { - every { groupedEvents.roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) } returns roomNotifications + coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications every { createSummaryNotification( - sessionId, + matrixUser, roomNotifications, invitationNotifications, simpleNotifications, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt index df0b5ad42b..b896737e6f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt @@ -17,11 +17,11 @@ package io.element.android.libraries.push.impl.notifications.fake 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.user.MatrixUser import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent -import io.mockk.every +import io.mockk.coEvery import io.mockk.mockk class FakeRoomGroupMessageCreator { @@ -29,14 +29,18 @@ class FakeRoomGroupMessageCreator { val instance = mockk() fun givenCreatesRoomMessageFor( - sessionId: SessionId, + matrixUser: MatrixUser, events: List, roomId: RoomId, - userDisplayName: String, - userAvatarUrl: String? ): RoomNotification.Message { val mockMessage = mockk() - every { instance.createRoomMessage(sessionId, events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage + coEvery { + instance.createRoomMessage( + currentUser = matrixUser, + events = events, + roomId = roomId, + ) + } returns mockMessage return mockMessage } }